在SwiftUI中使用WKWebView - 当用户与网站交互时,我该如何切换视图?

6
我的情况如下:我有一个SwiftUI应用程序,想要显示一个WebView。当用户在该WebView中点击某个按钮时,我希望用户被重定向到下一个(SwiftUI)视图。 我使用UIViewRepresentable,因为这似乎是在SwiftUI中显示WebView的当前方式,因为它是通往UIKit的桥梁。 问题是:UIViewRepresentable没有body。那么我在哪里告诉视图切换呢?在通常的SwiftUI视图中,我会有一个模型,我会更新该模型,然后在body上对模型更改进行反应。
我设置了一个示例,在其中 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()
    }
}

协调人 - 在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中。当用户点击“Google Search”时,UIViewControllerRepresentable成功调用了model.startCamera()。然而,这个模型的改变并没有反映在WebviewContainer中。为什么会这样?这不是@ObservedObject的全部目的吗?


你需要将一些可观察对象(例如通过 @ObservedObject@EnvironmentObject)传递到你的 Webview 中,并在 doSomething 中更改一些 @Published 属性,具体取决于你的 ContentView 显示的值(或导航到的值),以显示其他视图。 - Asperi
你能分享剩下的代码吗?我无法凑出一个可用的示例。 - krjw
代码缺少哪些部分?我能提供什么帮助? - Schnodderbalken
2个回答

5

我在提供的代码中添加了一个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()
    }
}

演示

我希望这有所帮助。


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

0

好的,现在我在经历了一番激烈的调试后,终于找到了问题:

1.) 我在这篇帖子中提供的代码确实能够正常工作。但是仅在我的周围代码的情况下会出现问题

2.) 迄今为止唯一提供的答案没有解决任何问题。只是因为它已经可以工作,并且与未在主线程上执行的代码无关(尽管我确实同意对影响UI的操作应该在主线程上执行)。

3.) 在我的情况下,问题出在导致WebviewContainer的视图中。在那个视图中,我有一个模型,该模型在API调用中更改其值。成功后,它决定重定向到WebviewContainer,如果失败,则不会。到此为止都很好。然而,我在if语句中进行了模型更改,并且应该在else中防止它。我错过了这个”else”,所以它短暂地做了正确的事情,然后又切回去了。这很难调试,因为当我观察模型时,一切都没问题。唯一奇怪的事情是模型的构造函数被调用了两次。

很抱歉,在这种情况下我无法给予所提供答案的赏金(您将因投入的时间而获得一次点赞)。

非常感谢!教训是:下次尽可能地隔离问题,以减少应用程序代码的副作用。


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