How to use WebView in SwiftUI? - swiftui

I have this code, but it raise many errors:
import SwiftUI
import WebKit
struct ContentView: View {
#State private var webView = WebView(request: URLRequest(url: URL(string: "https://github.com")!))
var body: some View {
VStack {
webView
}
}
}
struct WebView: UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
like: Generic struct 'VStack' requires that 'WebView' conform to 'View'

Your code works on my machine but you should probably use WebView inside your body.
Example:
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
WebView(request: URLRequest(url: URL(string: "https://github.com")!))
}
}

Related

SwiftUI - Spotify Auth Webview not loading in build but works in preview

I have a problem where my webView doesn't load on build but in preview it works as it should. What I mean by not loading is that the webView is just white.
What I first thought was that the simulated iphones network settings made it so it didn't allow URLRequests for some reason but I disputed this quickly when I temporarily changed the url to "https://google.com" and it loaded as it should.
Here is my code:
//
// ContentView.swift
// spotifystats
//
// Created by bappo on 2021-08-15.
//
import SwiftUI
import WebKit
struct SpotifyConstants {
static let CLIENT_ID = "***************"
static let SESSION_KEY = "spotifySessionKey"
static let REDIRECT_URI = "spotifystats://"
static let SCOPE = "user-read-email"
}
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
struct ContentView: View {
#State var isLogginIn = false
let authURLFull = "https://accounts.spotify.com/authorize?response_type=token&client_id=" + SpotifyConstants.CLIENT_ID + "&scope=" + SpotifyConstants.SCOPE + "&redirect_uri=" + SpotifyConstants.REDIRECT_URI + "&show_dialog=false"
var body: some View {
Button("Spotify Login") {
isLogginIn = true
}
.padding()
.foregroundColor(.white)
.background(Color.green)
.clipShape(Capsule())
.sheet(isPresented: $isLogginIn) {
WebView(request: URLRequest(url: URL(string: authURLFull)! ))
}
}
}
extension ContentView {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
RequestForCallbackURL(request: navigationAction.request)
}
func RequestForCallbackURL(request: URLRequest) {
let requestURLString = (request.url?.absoluteString)! as String
print(requestURLString)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How do I display WKWebView url?

I was wondering how I can display my current URL on WKWebView and display URL on change also. Currently I added a Text() but it does not display the URL at all. Any ideas how I can fix it?
WebView
import SwiftUI
import WebKit
struct WebView : UIViewRepresentable {
let request: URLRequest
private var webView: WKWebView?
init (request: URLRequest) {
self.webView = WKWebView()
self.request = request
}
func makeUIView(context: Context) -> WKWebView {
webView?.load(request)
return webView!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
func URL() -> String {
return (webView?.url)?.absoluteString ?? ""
}
func goBack() {
webView?.goBack()
}
func goForward() {
webView?.goForward()
}
}
MainView
struct MainView: View {
var webView: WebView = WebView(request: URLRequest(url: URL(string: "https://www.google.com")!))
var body: some View {
VStack {
//...
Text(webView.URL())
webView
Button(action: { webView.goBack() }, label: { Text("Test") })
//...
}
}
To keep track of the URL of the WKWebView, you'll need to use a WKNavigationDelegate.
You can use a Coordinator in your UIViewRepresentable for the WKNavigationDelegate and an ObservableObject with a #Published value to communicate between the WebView and your parent view:
class NavigationState : ObservableObject {
#Published var url : URL?
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
context.coordinator.navigationState = navigationState
webView.navigationDelegate = context.coordinator
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
class Coordinator : NSObject, WKNavigationDelegate {
var navigationState : NavigationState?
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
navigationState?.url = webView.url
}
}
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Text(navigationState.url?.absoluteString ?? "(none)")
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
}
}
}
Update, based on comments:
class NavigationState : NSObject, ObservableObject {
#Published var url : URL?
let webView = WKWebView()
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.url = webView.url
}
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeUIView(context: Context) -> WKWebView {
let webView = navigationState.webView
webView.navigationDelegate = navigationState
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Text(navigationState.url?.absoluteString ?? "(none)")
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
}
}
}
}

How to create a browser tab for WKWebView in SwiftUI

I am trying to build a new tab function but I am not too sure how I can accomplish this. I am having trouble setting a new or previous WKWebView. And also how do I display an errorView if the url is invalid?
This is what I have so far.
EDIT: I wasn't too sure how to initialize or how to create a invalidurl view. This is kind of like whats going on through my mind
class NavigationState : NSObject, ObservableObject {
#Published var url : URL?
let webView = WKWebView()
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.url = webView.url
}
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeUIView(context: Context) -> WKWebView {
let webView = navigationState.webView
webView.navigationDelegate = navigationState
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
#State var tablist = [NavigationState]
#State var validurl = true;
init(){
//does not work currently
navigationState.createNewWebView(withRequest: URLRequest(url: URL(string: "https://www.google.com")!))
}
var body: some View {
VStack(){
Button("create new tab"){
tablist.append(navigationState)
//create and set new webview
}
Text(navigationState.url?.absoluteString ?? "(none)")
if(validUrl){
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
} else{InvalidURL()}
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
TextField(){onCommit: {
navigationState.selectedWebView?.load(URLRequest(url: URL(string: urlInput)!))
}}
}
}
List {
ForEach(tabs, id: \.self) { tab in
Button(action: {
//set to current webview
}, label: {
Text(tab.webView.url)
})
}.onDelete(perform: delete)
}
}
}
EDIT for the initlization
I added this block of code underneath the NavigationState but I keep getting a blank screen.
override init(){
super.init()
let wv = WKWebView()
wv.navigationDelegate = self
self.webViews.append(wv)
self.selectedWebView = wv
wv.load(URLRequest(url: URL(string: "https://www.google.com")!))
}
Here's a relatively simple implementation (code first, then explanation):
class NavigationState : NSObject, ObservableObject {
#Published var currentURL : URL?
#Published var webViews : [WKWebView] = []
#Published var selectedWebView : WKWebView?
#discardableResult func createNewWebView(withRequest request: URLRequest) -> WKWebView {
let wv = WKWebView()
wv.navigationDelegate = self
webViews.append(wv)
selectedWebView = wv
wv.load(request)
return wv
}
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
if webView == selectedWebView {
self.currentURL = webView.url
}
}
}
struct WebView : UIViewRepresentable {
#ObservedObject var navigationState : NavigationState
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let webView = navigationState.selectedWebView else {
return
}
if webView != uiView.subviews.first {
uiView.subviews.forEach { $0.removeFromSuperview() }
webView.frame = CGRect(origin: .zero, size: uiView.bounds.size)
uiView.addSubview(webView)
}
}
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Button("create new tab"){
navigationState.createNewWebView(withRequest: URLRequest(url: URL(string: "https://www.google.com")!))
}
Text(navigationState.currentURL?.absoluteString ?? "(none)")
WebView(navigationState: navigationState)
.clipped()
HStack {
Button("Back") {
navigationState.selectedWebView?.goBack()
}
Button("Forward") {
navigationState.selectedWebView?.goForward()
}
}
List {
ForEach(navigationState.webViews, id: \.self) { tab in
Button(action: {
navigationState.selectedWebView = tab
}) {
Text(tab.url?.absoluteString ?? "?")
}
}
}
}
}
}
Instead of trying to store an array of NavigationStates, I refactored NavigationState to hold an array of web views. The current URL and selected web view are #Published values so that the parent view can see the URL, the selected view, etc.
WebView had to be changed significantly since it had to update which WKWebView is being shown at any given time.
This is pretty rough-around-the edges code. I'd do more refactoring if it were my own project, but this should get you started.
Regarding showing errors with invalid URLs, that's really a second question and probably needs more clarity (what constitutes an invalid URL? Where is it coming from? Do you mean just if the user enters one (in some part of the UI that you're not describing) or also if they click on an invalid link on the page?)

Access #Environment object from UIViewControllerRepresentable object

I used this approach to incorporate camera with swiftUI:
https://medium.com/#gaspard.rosay/create-a-camera-app-with-swiftui-60876fcb9118
The UIViewControllerRepresentable is implemented by PageFourView class. PageFourView is one of the TabView of the parental View. I have an #EnvironmentObject passed from the SceneDelegate to the parent view and then to PageFourView. But when I am trying to acess #EnvironmentObject from makeUIViewController method of PageFourView I get an error:
Fatal error: No ObservableObject of type Data found. A
View.environmentObject(_:) for Data may be missing as an ancestor of
this view
... even though I can see the #Environment object from context.environment. Here is my code:
import UIKit
import SwiftUI
import Combine
final class PageFourView: UIViewController, UIViewControllerRepresentable {
public typealias UIViewControllerType = PageFourView
#EnvironmentObject var data: Data
var previewView: UIView!
override func viewDidLoad() {
previewView = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PageFourView>) -> PageFourView {
print(context.environment)
print(self.data.Name)
return PageFourView()
}
func updateUIViewController(_ uiViewController: PageFourView, context: UIViewControllerRepresentableContext<PageFourView>) {
}
}
struct PageFourView_Previews: PreviewProvider {
#State static var data = Data()
static var previews: some View {
PageFourView().environmentObject(self.data)
}
}
here is the parental view that PageFourView is called from:
import SwiftUI
struct AppView: View {
#EnvironmentObject var data: Data
var body: some View {
TabView {
PageOneView().environmentObject(data)
.tabItem {
Text("PageOne")
}
PageTwoView().environmentObject(data)
.tabItem {
Text("PageTwo")
}
PageThreeView().environmentObject(data)
.tabItem {
Text("PageThree")
}
PageFourView().environmentObject(data)
.tabItem {
Text("PageFour")
}
}
}
}
struct AppView_Previews: PreviewProvider {
#State static var data = Data()
static var previews: some View {
AppView().environmentObject(self.data)
}
}
final class CameraViewController: UIViewController {
let cameraController = CameraController()
var previewView: UIView!
override func viewDidLoad() {
previewView = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
cameraController.prepare {(error) in
if let error = error {
print(error)
}
try? self.cameraController.displayPreview(on: self.previewView)
}
}
}
extension CameraViewController : UIViewControllerRepresentable{
public typealias UIViewControllerType = CameraViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> CameraViewController {
return CameraViewController()
}
public func updateUIViewController(_ uiViewController: CameraViewController, context: UIViewControllerRepresentableContext<CameraViewController>) {
}
}
And UIViewRepresentable and UIViewControllerRepresentable is-a View and must be a struct.
In described case controller representable is not needed, because you operate with view, so here is corrected code:
struct PageFourView: UIViewRepresentable {
#EnvironmentObject var data: Data
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height))
view.contentMode = UIView.ContentMode.scaleAspectFit
print(context.environment)
print(self.data.Name)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
btw, you don't need to pass .environmentObject to subviews in same view hierarchy, only for new hierarchy, like sheets, so you can use simplified code as below
var body: some View {
TabView {
PageOneView()
.tabItem {
Text("PageOne")
}
PageTwoView()
.tabItem {
Text("PageTwo")
}
PageThreeView()
.tabItem {
Text("PageThree")
}
PageFourView()
.tabItem {
Text("PageFour")
}
}
}
Update: for CameraViewController just wrap it as below
struct CameraView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CameraViewController {
CameraViewController()
}
func updateUIViewController(_ uiViewController: CameraViewController, context: Context) {
}
}

SwiftUI How can I add an activity indicator in WKWebView?

How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?
I've looked at some of the old posts but could not figure out how to do it in SwiftUI
see link to one of the old solutions below
How to add Activity Indicator to WKWebView (Swift 3)
Use UIViewRepresentable to create a UIActivityIndicatorView:
You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.
You can set the color of the activity indicator by using the color property.
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Create a LoadingView to allow you to wrap around your views:
This allows you to style the activity views content.
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.red)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:
Based on this answer: https://stackoverflow.com/a/58825642/264802
class WebViewModel: ObservableObject {
#Published var url: String
#Published var isLoading: Bool = true
init (url: String) {
self.url = url
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeCoordinator() -> Coordinator {
Coordinator(self.viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.viewModel.isLoading = false
}
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
func makeUIView(context: Context) -> UIView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: self.viewModel.url) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
}
Then to use it inside your views you would do the following:
struct ContentView: View {
#StateObject var model = WebViewModel(url: "http://www.google.com")
var body: some View {
LoadingView(isShowing: self.$model.isLoading) {
WebView(viewModel: self.model)
}
}
}
Using 3 Steps I do it in my project.
Step 1: Create a Loading View
import SwiftUI
import UIKit
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
// Main View
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
let message: String
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text(self.message)
.bold()
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
// Mark: Testing
struct LoadingIndicator: View {
var body: some View {
LoadingView(isShowing: .constant(true), message: "Loading...") {
NavigationView {
List(["1", "2", "3", "4", "5"], id: \.self) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}
}
}
}
struct ActivityIndicatorView_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}
Step 2: Create a WebView and WebViewModel
import SwiftUI
import WebKit
class WebViewModel: ObservableObject {
#Published var isLoading: Bool = false
}
struct WebView: UIViewRepresentable {
#ObservedObject var webViewModel: WebViewModel
let urlString: String
func makeUIView(context: Context) -> WKWebView {
let wkWebView = WKWebView()
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url: url)
wkWebView.load(urlRequest)
}
return wkWebView
}
func updateUIView(_ wkWebView: WKWebView, context: Context) {
// do nothing
}
class Coordinator: NSObject, WKNavigationDelegate {
let webViewModel: WebViewModel
init(_ webViewModel: WebViewModel) {
self.webViewModel = webViewModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webViewModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webViewModel.isLoading = false
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(webViewModel)
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(webViewModel: WebViewModel(),
urlString: "https://instagram.com/mahmudahsan/")
}
}
Step 3: In your main view use the following code to show indicator and webview
ZStack {
WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
.frame(height: 1000)
if webViewModel.isLoading {
LoadingView(isShowing: .constant(true), message: "Loading...") {
EmptyView()
}
}
}