简体   繁体   中英

WKWebView in SwiftUI - How do I switch the view when user interacts with the website?

My situation is the following: I have a SwiftUI application and want to display a WebView. When the user taps a certain button in that WebView I want the user to be redirected to the next (SwiftUI) view. I use a UIViewRepresentable as this seems to be the current way to go for showing a WebView in SwiftUI because it's the bridge to UIKit. The problem is: UIViewRepresentable has no body . So where do I tell the view to switch over? In usual SwiftUI views I would have a model which I'd update and then react on the model change in the body.

I set up an example in which https://www.google.com is rendered in the WebView. When the user sends a search query the coordinator is called which calls a function of the UIViewRepresentable:

View - This is the view that should react on the model changes by displaying another view (implemented with 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 - This is necessary in order to use the WKWebview in SwiftUI context

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 - The WKNavigationDelegate that reacts on the click on "Google Search" and calls the coordinator

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 - Bridge between WKNavigationDelegate and UIViewRepresentable

import Foundation
import WebKit

class Coordinator: NSObject {
    var webview: Webview

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

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

UPDATE

I now have a View with a model ( @ObservedObject ). This model is given to the UIViewControllerRepresentable . When the user clicks "Google Search", UIViewControllerRepresentable successfully calls model.startCamera() . However, this change of the model is not reflected in the WebviewContainer . Why is that? Isn't that the whole purpose of @ObservedObject s?

I added a Model to the provided code, which is updated when the startCamera() function is called. @Published variables should be updated on the UI thread since in most cases they change UI state which causes the UI to update.

Here is the full example:

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

演示

I hope this helps.

Okay now, I figured it out after having a quite intense debug session:

1.) The code I've presented in this post does indeed work. It was only problematic in the context of my surrounding code .

2.) The only provided answer so far does not fix anything . Just because it's already working and has nothing to do with code not being executed on the main thread (although I certainly agree, that this should be done for actions that affect the UI).

3.) In my case the problem was in the view that leads to WebviewContainer . In that view I had a model that was changing its values in an API call. On success it decides to redirect to WebviewContainer in case of failure it doesn't. So far so good. However, I was doing the model change in the if and should have prevented it in else . I was missing the else so for a blink of a second it was doing the right thing and then switching back. This was hard to debug because when I watched the model, everything was finde. The only thing that was strange was that the constructor of the model was called twice.

I am sorry that in this case I can not give the bounty to the given answer (you will get an upvote for the time invested.

Thank you very much: Learning. next time try to isolate the issue as much as I can in order to decrease the side effects of the rest of my application code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM