SwiftUI WKWebView 检测到 url 变化

SwiftUI WKWebView detect url changing

我是 swift 学习者。我使用 SwiftUI,它是一个结构,我必须实现一个 WKWebView,其中 url 正在动态变化。我必须抓住这些变化 urls,但我尝试过的解决方案不起作用。

例如: 我试过这个代码块,但它不工作,它给了我一些编译器错误:

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()
    }
}

我在 struct ContentView...

行有一个非 class 类型 'ContentView' cannot conform to class protocol 'NSObjectProtocol' 错误

您将此用于 WKNavigationProtocol 的委托来执行(例如允许或取消 URL 加载)您的操作

    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)
}

我找到了一个很好的解决我的问题的方法。我会 post 在这里。也许有人想看,也许对他们有用。

observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
    if let url = view.url {
        // do something with your url
    }
}

您可以使用 key/value 观察来检测 WKWebView 的 url 属性 的变化。

这是一个将 WKWebView 包装在 UIViewRepresentable 中的简单示例。

请注意,因为我们正在修改 属性,UIViewRepresentable 是一个 final class 而不是结构。

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
    }
}

然后您可以在持有 WebView 的容器的 onReceive() 中收听 url 修改后的通知:

.onReceive(self.webview.$url) { url in
                    if let url = url {
                }
}

您可以简单地创建一个名为“WebViewModel”的 webview 的 ObservableObject 模型class,例如

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

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

并导入

import WebKit
import Combine

然后复制这段代码

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")!))
    }
}

在你看来

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 
                }
        }
   }}

所以通过这种方式你可以得到其他代表的回应。

使用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)
}

您可以阅读来自 Mediumthis 文章,其中显示了拦截每个 network 调用或 Url 更改和获取即将到来的 Url 相关的更好方法data。它还展示了如何在 SwiftUI 中实现 WebView、与 JavaScript 函数交互以及从 iOS 项目

加载本地 .html 文件

我来这里是为了尝试一种快速的方法来在 SwiftUI 中获取工作示例,以从 Web 身份验证服务获取 HTML 响应。 (具体来说,新的 DropBox awful 使用 URI 的身份验证模式......我们没有看到这个细节,但回调和代码应该足够解释。(JSOn来自我在 URI 中指定的 Web 服务器) )

在我们的 Swift UI 部分:

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
                        } )
                    }
        }
    }
}

我们的实施:

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) { }
}

一些辅助代码,让生活更轻松:

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)
    
}

简单,就用这个委托方法

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print(webView.url?.absoluteString)
    }