SwiftUI WKWebView 检测 URL 变化

11

我学习能力强。我使用的是SwiftUI,它是一个结构体。我必须实现一个WKWebView,并且其中的url会动态更改。我需要捕获这些变化的url,但是我尝试的解决方案并没有起作用。

例如:https://dev59.com/KFYN5IYBdhLWcg3wNFp4#48273950 我尝试了这段代码,但它没有起作用,并且给我一些编译器错误:

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

在结构体 ContentView 的行中,我遇到了一个“非类类型 'ContentView' 不能符合类协议 'NSObjectProtocol' ”的错误。

Translated:

I encountered an error "Non-class type 'ContentView' cannot conform to class protocol 'NSObjectProtocol'" at the line of struct ContentView.

7个回答

20

您可以简单地创建一个名为“WebViewModel”的ObservableObject模型类,用于webview,如下所示:

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

这样你就可以获得其他代表的反应。


2
谢谢,这是非常好的答案,但是现在我不会在生产中使用SwiftUI。我回到了Storyboard。 - Alparslan Selçuk Develioğlu
2
@Akhtar,你能否修复AnyView中的缩进? - Jonny
1
对我有用 :) - Sogeking
我不确定为什么在这种情况下需要使用Combine框架。 - akaakoz
@akaakoz 这是因为每当页面重新加载时,它需要被观察。 - Akhtar

5

您可以使用它来委派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)
}

我该如何在SwiftUI中实现这些功能?我需要代码示例。我是Swift世界的新手。 - Alparslan Selçuk Develioğlu
看一下答案,我已经更新了确切的委托方法示例 @AlparslanSelçukDevelioğlu - umer farooqi
1
问题在于:你不能在SWIFTUI中这样写。因为你不能这样写:struct SwiftUIView: UIViewRepresentable, WKNavigationDelegate 在结构体中,你不能继承两个类。你必须内联编写此函数。整个互联网都充满了你的答案,但还是谢谢你的努力。我找到了解决方案。 - Alparslan Selçuk Develioğlu

2

简单易懂,只需使用这个委托方法

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

这个解决方案如何解决OP的问题?(与其他答案有何不同?) - ryanwebjackson
WKWebView有一个委托方法"didFinish",在成功加载URL时触发。此外,在URL成功加载后,WKWebView的URL会得到更新。因此,一种非常简单的方法是在didFinish委托方法中通过webView.url获取加载后的新URL。希望我已经回答了您的问题! :) - Muhamad Jawdat Salem Akoum

2
使用以下`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)
}

您可以阅读来自 Medium 的 this 文章,其中展示了更好的拦截每个网络调用或 URL 更改并获取即将到来的 URL 相关数据的方法。它还展示了如何在 SwiftUI 中实现 WebView,与 JavaScript 函数进行交互,并从 iOS 项目加载本地 .html 文件。

1

我来这里是为了尝试在SwiftUI中快速获取一个工作示例,从Web身份验证服务获取HTML响应(具体来说是使用URI的新DropBox 糟糕身份验证模式...我们看不到这些细节,但回调和代码应该足够说明问题。(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)
    
}

1
你可以使用键/值观察来检测 WKWebView 的 url 属性的更改。
下面是将 WKWebView 包装在 UIViewRepresentable 中的简单示例。
请注意,由于我们正在修改属性,因此 UIViewRepresentable 是一个 final 类而不是结构体。
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 {
                }
}

-1

我已经找到了一个非常好的解决方案来回答我的问题。我将在这里发布它。也许有人想看看它,对他们可能会有用。

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

2
抱歉,但这个答案没有实现的描述。"observe.observation"?你从哪里得到的?你把它放在哪里?能否提供一个实际完整的工作代码? - Ryuuzaki Julio
你说得对。但不幸的是,我现在无法访问那个项目和代码了。工作变了。 - Alparslan Selçuk Develioğlu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接