SwiftUI WKWebView detect url changing
Asked Answered
L

7

11

I'm a swift learner. I work with SwiftUI which is a struct, I have to implement a WKWebView and in that, a url is changing dynamically. I have to catch these changing urls, but solutions I have tried are not working.

For example: https://mcmap.net/q/794050/-how-can-i-detect-when-url-of-amp-page-changed-with-wkwebview I tried this code block but it is not working and it gives me some compiler errors:

import SwiftUI
import WebKit

struct ContentView: UIViewRepresentable, WKNavigationDelegate {

    let request = URLRequest(url: URL(string: "https://apple.com")!)

    func makeUIView(context: Context) -> WKWebView  {
    let preferences = WKPreferences()
    preferences.javaScriptEnabled = true
    preferences.javaScriptCanOpenWindowsAutomatically = true

    let configuration = WKWebViewConfiguration()
    configuration.preferences = preferences
    let webView = WKWebView(frame: .zero, configuration: configuration)
    webView.allowsBackForwardNavigationGestures = true


    return webView
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 
 // 'override' can only be specified on class membe
  if keyPath == #keyPath(WKWebView.url) {
    print("### URL:", self.webView.url!)
  }

  if keyPath == #keyPath(WKWebView.estimatedProgress) {
    // When page load finishes. Should work on each page reload.
    if (self.webView.estimatedProgress == 1) {
      print("### EP:", self.webView.estimatedProgress)
    }
  }
}

func updateUIView(_ uiView: WKWebView, context: Context) {
    uiView.load(request)
}

func webViewDidFinishLoad(webView : WKWebView) {
    print("Loaded: \(String(describing: webView.url))")
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    print("Loaded: \(String(describing: webView.url))")
    //progressView.isHidden = true
}

func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
    //progressView.isHidden = false
    print("Loaded: \(String(describing: webView.url))")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I have a Non-class type 'ContentView' cannot conform to class protocol 'NSObjectProtocol' error at line struct ContentView...

Lippmann answered 11/10, 2019 at 7:41 Comment(0)
L
-1

I have found a very good solution to my question. I will post it here. Maybe someone wants to see it and might be useful to them.

observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
    if let url = view.url {
        // do something with your url
    }
}
Lippmann answered 11/10, 2019 at 18:3 Comment(2)
Sorry but this answer has no description of how to implement. "observe.observation"? where you get that from? Where do you put that in? How about an actual complete working code?Phalan
You are right. But unfortunately I don't have access that project and code now. Job changedPortis
T
20

You can simply create a ObservableObject model class of webview with the name "WebViewModel" like

class WebViewModel: ObservableObject {
    @Published var link: String
    @Published var didFinishLoading: Bool = false

    init (link: String) {
        self.link = link
    }
} 

and also import

import WebKit
import Combine

and then copy this code snippets

struct SwiftUIWebView: UIViewRepresentable {
    @ObservedObject var viewModel: WebViewModel

    let webView = WKWebView()

    func makeUIView(context: UIViewRepresentableContext<SwiftUIWebView>) -> WKWebView {
        self.webView.navigationDelegate = context.coordinator
        if let url = URL(string: viewModel.link) {
            self.webView.load(URLRequest(url: url))
        }
        return self.webView
    }

    func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<SwiftUIWebView>) {
        return
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        private var viewModel: WebViewModel

        init(_ viewModel: WebViewModel) {
            self.viewModel = viewModel
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            //print("WebView: navigation finished")
            self.viewModel.didFinishLoading = true
        }
    }

    func makeCoordinator() -> SwiftUIWebView.Coordinator {
        Coordinator(viewModel)
    }
}



struct SwiftUIWebView_Previews: PreviewProvider {
    static var previews: some View {
        
        SwiftUIWebView(viewModel: WebViewModel(link: "https://google.com"))
        //WebView(request: URLRequest(url: URL(string: "https://www.apple.com")!))
    }
}

and in you view

struct AnyView: View {
    @ObservedObject var model = WebViewModel(link: "https://www.wikipedia.org/")

    
var body: some View {
        
        
    NavigationView {
       SwiftUIWebView(viewModel: model)
                if model.didFinishLoading {
                    //do your stuff 
                }
        }
   }}

so in this way you can get the others delegates response.

Then answered 24/11, 2019 at 14:14 Comment(5)
Thank you this is very good answer but for now I will not use SwiftUI in production. I went back to Storyboard.Portis
@Then Can you please fix up the indentation in the AnyView?Connor
Worked for me :)Whelm
I'm not sure why Combine framework is needed for the use case.Alyworth
@Alyworth it's because whenever the page get relaod it needs to be observed.Then
T
5

you use this to delegates of WKNavigationProtocol to perform(e.g to allow or cancel URL Loading) your action

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let host = navigationAction.request.url?.host {
        if host.contains("facebook.com") {
            decisionHandler(.cancel)
            return
        }
    }

    decisionHandler(.allow)
}
Tacye answered 11/10, 2019 at 12:2 Comment(3)
How can I implement these in SwiftUI? I need code sample. I am new to Swift worldPortis
check out the answer, I've updated it with exact delegate method example @AlparslanSelçukDevelioğluTacye
The problem is this: You can not write this way in SWIFTUI. Because you can not write this: struct SwiftUIView: UIViewRepresentable, WKNavigationDelegate In struct, you can not inherit 2 classes. You have to write this function inline. Whole internet full of your answer but thanks for your effort. I found solution.Portis
H
2

Use the following delegate function of WKWebView:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  // Suppose you don't want your user to go a restricted site
  if let host = navigationAction.request.url?.host {
      if host == "restricted.com" {
          decisionHandler(.cancel)
          return
      }
  }
  decisionHandler(.allow)
}

You can read this article from Medium which shows a better way of intercepting every network call or Url changing and obtaining upcoming Url related data. It also shows how to implement WebView in SwiftUI, interfacing with JavaScript functions and loading a local .html file from iOS project

Hectogram answered 10/5, 2020 at 2:17 Comment(0)
B
2

Simple, Just use this delegate method

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print(webView.url?.absoluteString)
    }
Blayne answered 10/9, 2021 at 9:11 Comment(2)
How does this solve OP's question? (and differently from the other answers?)Macdougall
the wkwebview has a delegate method "didFinish" which triggers on successful url loading. Also on successful url loading, wkwebview url gets updated. So a very simple way is to get the new url after it loads through web view.url on didFinish delegate method. I hope I have answered your question! :)Blayne
F
1

You can use key/value observation to detect changes to the url property of the WKWebView.

Here is a simple example of wrapping a WKWebView in a UIViewRepresentable.

Note that because we are modifying a property, the UIViewRepresentable is a final class rather than a struct.

import Combine
import SwiftUI
import WebKit

final class WebView: UIViewRepresentable {

    @Published var url: URL? = nil {
        didSet {
            if url != nil {
                willChange.send(url)
            }
        }
    }

    private let view = WKWebView()

    private var urlChangedObservation: NSKeyValueObservation?
    private let willChange = PassthroughSubject<URL?, Never>()

    func makeUIView(context: Context) -> WKWebView {
        return makeWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
    }

    func display(_ html: String) {
        self.view.loadHTMLString(html, baseURL: nil)
    }

    public func load(_ url: String) -> WebView {
        let link = URL(string: url)!
        let request = URLRequest(url: link)
        self.view.load(request)
        return self
    }

    func makeWebView() -> WKWebView {
        self.urlChangedObservation = self.view.observe(\WKWebView.url, options: .new) { view, change in
            if let url = view.url {
                self.url = url
            }
        }
        return self.view
    }
}

You can then listen to the url modified notification in the onReceive() of the container holding the WebView:

.onReceive(self.webview.$url) { url in
                    if let url = url {
                }
}
Faires answered 2/11, 2019 at 18:22 Comment(1)
Are u sure that WebView can be a class for put it in SwiftUI code? I got error bcs of that...Glasswort
A
1

I came here trying a fast way to get a working sample in SwiftUI to get an HTML response from an web auth service. (in the specific the new DropBox awful auth schema using an URI... we do no see this details, but call-backs and code should be explanatory enough. (JSOn comes from my web server specified in URI) )

in our Swift UI part:

struct ContentView: View {
    @State private var showingSheet = false

    private var webCallBack: WebCallBack = nil

    let webView = WKWebView(frame: .zero)
    @State private var auth_code = ""
    
    var body: some View {
        VStack{
            Text("\(auth_code)")
               .font(.system(size: 50))
            Button("Show Auth web form") {
                        self.showingSheet = true
                    }
                    .sheet(isPresented: $showingSheet) {
                        WebView( webView: webView, webCallBack: { (d: Dict?) in
                            print("\n", d)
                            auth_code = (d?["auth_code"] as? String) ?? "!!"
                            showingSheet = false
                        } )
                    }
        }
    }
}

Our implementation:

typealias WebCallBack = ( (Dict?)->() )?

class MyWKDelegate: NSObject, WKNavigationDelegate{
        
    private var webCallBack : WebCallBack = nil
    
    init(webCallBack: WebCallBack) {
        self.webCallBack = webCallBack
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("End loading")
        
        webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { result, error in
            
            if let html = result as? String {
                //print(html)
                // we are here also at first call, i.e. web view with user / password. Custiomize as needed.
                if let d = dictFromJSONWith(string: html){
                    //print(d)
                    self.webCallBack?(d)
                }
            }
        })
    }
}

    
struct WebView: UIViewRepresentable {

    let webView: WKWebView
    let delegate: MyWKDelegate

    internal init(webView: WKWebView, webCallBack: WebCallBack) {
        self.webView = webView
        self.delegate = MyWKDelegate(webCallBack: webCallBack)

        webView.navigationDelegate = delegate
        
        let urlStr = DB_URL.replacingOccurrences(of: "APP_KEY", with: APP_KEY).replacingOccurrences(of: "REDIRECT_URI", with: REDIRECT_URI)
        print(urlStr)

        if let url = URL(string: urlStr){
        webView.load(URLRequest(url: url))
        }

    }


    func makeUIView(context: Context) -> WKWebView {
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) { }
}

Some accessory code to make life easier:

typealias Dict = [String : Any]
typealias Dicts = [Dict]


func dictFromJSONWith(data: Data?)->Dict? {
    
    guard let data = data else {
        return nil
    }
    if let dict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions() ){
        return dict as? Dict
    }
    return nil
}


func dictFromJSONWith(string: String?)->Dict?{
    
    guard let data = string?.data(using: .utf8) else{
        return nil
    }
    return dictFromJSONWith(data: data)
    
}
Arboriculture answered 17/8, 2021 at 16:43 Comment(0)
L
-1

I have found a very good solution to my question. I will post it here. Maybe someone wants to see it and might be useful to them.

observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
    if let url = view.url {
        // do something with your url
    }
}
Lippmann answered 11/10, 2019 at 18:3 Comment(2)
Sorry but this answer has no description of how to implement. "observe.observation"? where you get that from? Where do you put that in? How about an actual complete working code?Phalan
You are right. But unfortunately I don't have access that project and code now. Job changedPortis

© 2022 - 2024 — McMap. All rights reserved.