6

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 @ObservedObjects?

3
  • You need to pass into your Webview some observable object (eg. via @ObservedObject or @EnvironmentObject) and in doSomething change some @Published property, depending on which value your ContentView shows (or navigates to) some other View. Commented Nov 19, 2019 at 9:36
  • Could you share the rest of the code? I can not scrape together a working example Commented Nov 21, 2019 at 13:57
  • What parts of the code are missing? How can I help? Commented Nov 22, 2019 at 7:48

2 Answers 2

5
+75

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

Demo

I hope this helps.

Sign up to request clarification or add additional context in comments.

4 Comments

So the only change you made was wrapping the modification of the variable in a wrapper that tells its body to be executed on the main thread? I'm sorry, but this does not work for me :(. I see the print ("started Camera") on the console but the view won't switch. Actually I would be surprised if this was necessary. DispatchQueue is a class that belongs to UIKit whereas the View to be presented is made with SwiftUI. But maybe I am missing any other change you made? Can you please elaborate? :) Thank you!
Well the changes of a @Published variable need to be performed on the UI thread since they manipulate the view.
@Schnodderbalken i added the complete example ... when i search something and press the search Icon it moves to the CameraView
I wrote an answer myself! Thank you for your effort.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.