SwiftUI 中的 WKWebView - 用户与网站交互时如何切换视图?

Posted

技术标签:

【中文标题】SwiftUI 中的 WKWebView - 用户与网站交互时如何切换视图?【英文标题】:WKWebView in SwiftUI - How do I switch the view when user interacts with the website? 【发布时间】:2019-11-19 08:22:29 【问题描述】:

我的情况如下:我有一个 SwiftUI 应用程序并且想要显示一个 WebView。当用户点击该 WebView 中的某个按钮时,我希望将用户重定向到下一个(SwiftUI)视图。 我使用 UIViewRepresentable,因为这似乎是在 SwiftUI 中显示 WebView 的当前方式,因为它是通往 UIKit 的桥梁。 问题是:UIViewRepresentable 没有body。那么我在哪里告诉视图切换呢?在通常的 SwiftUI 视图中,我将有一个模型,我会对其进行更新,然后对主体中的模型更改做出反应。

我设置了一个示例,其中https://www.google.com 在 WebView 中呈现。当用户发送搜索查询时,会调用协调器,该协调器会调用 UIViewRepresentable 的函数:

View - 这是应该通过显示另一个视图来对模型更改做出反应的视图(使用 NavigationLinks 实现)

import SwiftUI

struct WebviewContainer: View 
    @ObservedObject var model: WebviewModel = WebviewModel()
    var body: some View 
        return NavigationView 
            VStack 
                NavigationLink(destination: LoginView(), isActive: $model.loggedOut) 
                    EmptyView()
                .isDetailLink(false)
                .navigationBarTitle(Text(""))
                .navigationBarHidden(self.model.navbarHidden)

                NavigationLink(destination: CameraView(model: self.model), isActive: $model.shouldRedirectToCameraView) 
                    EmptyView()
                
                .navigationBarTitle(Text(""))
                .navigationBarHidden(self.model.navbarHidden)
                Webview(model: self.model)
            
        
    

UIViewControllerRepresentable - 这是在 SwiftUI 上下文中使用 WKWebview 所必需的

import SwiftUI
import WebKit

struct Webview : UIViewControllerRepresentable 
    @ObservedObject var model: WebviewModel

    func makeCoordinator() -> Coordinator 
        Coordinator(self)
    

    func makeUIViewController(context: Context) -> EmbeddedWebviewController 
        let webViewController = EmbeddedWebviewController(coordinator: context.coordinator)
        webViewController.loadUrl(URL(string:"https://www.google.com")!)

        return webViewController
    

    func updateUIViewController(_ uiViewController: EmbeddedWebviewController, context: UIViewControllerRepresentableContext<Webview>) 

    

    func startCamera() 
        model.startCamera()
    

UIViewController - WKNavigationDelegate 对点击“Google 搜索”做出反应并调用协调器

import UIKit
import WebKit

class EmbeddedWebviewController: UIViewController, WKNavigationDelegate 

    var webview: WKWebView
    var router: WebviewRouter? = nil

    public var delegate: Coordinator? = nil

    init(coordinator: Coordinator) 
        self.delegate = coordinator
        self.webview = WKWebView()
        super.init(nibName: nil, bundle: nil)
    

    required init?(coder: NSCoder) 
        self.webview = WKWebView()
        super.init(coder: coder)
    

    public func loadUrl(_ url: URL) 
        webview.load(URLRequest(url: url))
    

    override func loadView() 
        self.webview.navigationDelegate = self
        view = webview
    

    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) 
        guard let url = (navigationResponse.response as! HTTPURLResponse).url else 
            decisionHandler(.cancel)
            return
        

        if url.absoluteString.starts(with: "https://www.google.com/search?") 
            decisionHandler(.cancel)
            self.delegate?.startCamera(sender: self.webview)
        
        else 
            decisionHandler(.allow)
        
    

    override func viewDidLoad() 
        super.viewDidLoad()
    

Coordinator - WKNavigationDelegateUIViewRepresentable

之间的桥梁
import Foundation
import WebKit

class Coordinator: NSObject 
    var webview: Webview

    init(_ webview: Webview) 
        self.webview = webview
    

    @objc func startCamera(sender: WKWebView) 
        webview.startCamera()
    

更新

我现在有一个带有模型的视图 (@ObservedObject)。该模型被赋予 UIViewControllerRepresentable。当用户点击“谷歌搜索”时,UIViewControllerRepresentable成功调用model.startCamera()。但是,模型的这种变化并没有体现在 WebviewContainer 中。这是为什么?这不是 @ObservedObject 的全部目的吗?

【问题讨论】:

您需要将一些可观察对象(例如,通过@ObservedObject@EnvironmentObject)传递到您的Webview 并在doSomething 中更改一些@Published 属性,具体取决于您的ContentView 显示的值(或导航到)其他一些视图。 你能分享剩下的代码吗?我无法拼凑出一个可行的例子 缺少哪些代码部分?我能提供什么帮助? 【参考方案1】:

我在提供的代码中添加了一个Model,它会在调用startCamera() 函数时更新。 @Published 变量应该在 UI 线程上更新,因为在大多数情况下它们会更改 UI 状态,从而导致 UI 更新。

这是完整的例子:

import SwiftUI
import Foundation
import WebKit

class Coordinator: NSObject 
    var webview: Webview

    init(_ webview: Webview) 
        self.webview = webview
    

    @objc func startCamera(sender: WKWebView) 
        webview.startCamera()
    


class EmbeddedWebviewController: UIViewController, WKNavigationDelegate 

    var webview: WKWebView
    //var router: WebviewRouter? = nil

    public var delegate: Coordinator? = nil

    init(coordinator: Coordinator) 
        self.delegate = coordinator
        self.webview = WKWebView()
        super.init(nibName: nil, bundle: nil)
    

    required init?(coder: NSCoder) 
        self.webview = WKWebView()
        super.init(coder: coder)
    

    public func loadUrl(_ url: URL) 
        webview.load(URLRequest(url: url))
    

    override func loadView() 
        self.webview.navigationDelegate = self
        view = webview
    

    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) 
        guard let url = (navigationResponse.response as! HTTPURLResponse).url else 
            decisionHandler(.cancel)
            return
        

        if url.absoluteString.starts(with: "https://www.google.com/search?") 
            decisionHandler(.cancel)
            self.delegate?.startCamera(sender: self.webview)
        
        else 
            decisionHandler(.allow)
        
    

    override func viewDidLoad() 
        super.viewDidLoad()
    


class WebviewModel: ObservableObject 
    @Published var loggedOut: Bool = false
    @Published var shouldRedirectToCameraView: Bool = false
    @Published var navbarHidden: Bool = false
    func startCamera() 
        print("Started Camera")
        DispatchQueue.main.async 
            self.shouldRedirectToCameraView.toggle()
        
    


struct Webview : UIViewControllerRepresentable 
    @ObservedObject var model: WebviewModel

    func makeCoordinator() -> Coordinator 
        Coordinator(self)
    

    func makeUIViewController(context: Context) -> EmbeddedWebviewController 
        let webViewController = EmbeddedWebviewController(coordinator: context.coordinator)
        webViewController.loadUrl(URL(string:"https://www.google.com")!)

        return webViewController
    

    func updateUIViewController(_ uiViewController: EmbeddedWebviewController, context: UIViewControllerRepresentableContext<Webview>) 

    

    func startCamera() 
        model.startCamera()
    


struct LoginView: View 
    var body: some View 
        Text("Login")
    


struct CameraView: View 
    @ObservedObject var model: WebviewModel
    var body: some View 
        Text("CameraView")
    


struct WebviewContainer: View 
    @ObservedObject var model: WebviewModel = WebviewModel()
    var body: some View 
        return NavigationView 
            VStack 
                NavigationLink(destination: LoginView(), isActive: $model.loggedOut) 
                    EmptyView()
                .isDetailLink(false)
                .navigationBarTitle(Text("Hallo"))
                .navigationBarHidden(self.model.navbarHidden)

                NavigationLink(destination: CameraView(model: self.model), isActive: $model.shouldRedirectToCameraView) 
                    EmptyView()
                
                .navigationBarTitle(Text(""))
                .navigationBarHidden(self.model.navbarHidden)
                Webview(model: self.model)
            
        
    



struct ContentView: View 

    var body: some View 
        WebviewContainer()
    


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


我希望这会有所帮助。

【讨论】:

所以你所做的唯一改变是将变量的修改包装在一个包装器中,告诉它的主体在主线程上执行?很抱歉,但这对我不起作用:(。我在控制台上看到打印(“已启动的相机”),但视图不会切换。实际上,如果这是必要的,我会感到惊讶。DispatchQueue 是一个类属于 UIKit,而要呈现的 View 是用 SwiftUI 制作的。但也许我错过了你所做的任何其他更改?你能详细说明一下吗?:) 谢谢! @Published 变量的更改需要在 UI 线程上执行,因为它们会操纵视图。 @Schnodderbalken 我添加了完整的示例......当我搜索某些内容并按下搜索图标时,它会移动到 CameraView 我自己写了一个答案!感谢您的努力。【参考方案2】:

好的,我在进行了相当激烈的调试后才弄清楚:

1.) 我在这篇文章中提供的代码确实有效。 这只是在我周围的代码上下文中存在问题

2.) 迄今为止唯一提供的答案不能解决任何问题。仅仅因为它已经在工作并且与不在主线程上执行的代码无关(尽管我当然同意,这应该针对影响 UI 的操作执行)。

3.) 就我而言,问题出在导致 WebviewContainer 的视图中。在那个视图中,我有一个模型在 API 调用中改变了它的值。成功时它决定重定向到 WebviewContainer 以防失败。到现在为止还挺好。但是,我在 if 中进行了模型更改,并且应该在 else 中阻止它。我错过了其他,所以有一瞬间它正在做正确的事情,然后又切换回来。这很难调试,因为当我查看模型时,一切都已找到。唯一奇怪的是模型的构造函数被调用了两次。

很抱歉,在这种情况下,我无法为给定的答案提供赏金(您将获得对所投入时间的支持。

非常感谢!学习:下次尝试尽可能多地隔离问题,以减少其余应用程序代码的副作用。

【讨论】:

以上是关于SwiftUI 中的 WKWebView - 用户与网站交互时如何切换视图?的主要内容,如果未能解决你的问题,请参考以下文章

iOS SwiftUI App Clip WKWebView 和 SFSafariViewController 不起作用

在 SwiftUI 中关闭 WKWebView

SwiftUI:WKWebView 在 macOS 上截断页面内容(NSViewRepresentable)

如何在 SwiftUI 中观察 WKWebView 的加载进度?

SwiftUI:使用 WkWebView 突出显示具有不同颜色的文本

SwiftUI - WKWebView 的 iOS 13 UIViewRepresentable 获取线程 1:EXC_BREAKPOINT 崩溃