Search code examples
swiftuiwkwebview

Send update to child view (WKWebView) using swiftUI?


I would like to make a very simple browser in a View using SwiftUI.

What to expect:

In my ContentView, there is a WKWebView, and a "Go Back" button on the left of the Navigation Bar. If "Go Back" button is pressed, the webView will go back to previous page.

ContentView.swift:

struct ContentView: View {
    let defaultUrl = "https://www.apple.com"
    @State var needsGoBack = false
    
    var body: some View {
        WebView(urlString: defaultUrl, needsGoBack: $needsGoBack)
            .navigationBarItems(leading:
                Button(action: {
                    print("button pressed...set needsGoBack = true")
                    needsGoBack = true
                }) {
                    Text("Go Back")
                })
    }
}

WebView.swift

struct WebView: UIViewRepresentable {
    
    let urlString: String
    let navigationHelper = WebViewHelper()
    @State var myWebView = WKWebView()
    @Binding var needsGoBack: Bool
    
    func makeUIView(context: Context) -> WKWebView {
        if let url = URL(string: urlString) {
            let request = URLRequest(url: url)
            myWebView.load(request)
        }
        
        myWebView.navigationDelegate = navigationHelper
        myWebView.uiDelegate = navigationHelper
        
        return myWebView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        print("webview updateUIView")
        
        print("needsGoBack", needsGoBack)
        
        if needsGoBack {
            myWebView.goBack()
            needsGoBack = false // this line has problem
        }
    }
    
    typealias UIViewType = WKWebView
    
}

// https://gist.github.com/joshbetz/2ff5922203240d4685d5bdb5ada79105
class WebViewHelper: NSObject, WKNavigationDelegate, WKUIDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("webview didFinishNavigation")
    }
    
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("didStartProvisionalNavigation")
    }
    
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        print("webviewDidCommit")
    }
    
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        print("didReceiveAuthenticationChallenge")
        completionHandler(.performDefaultHandling, nil)
    }
}

What actually happens:

In the updateUIView function of the WebView.swift, there is a caution message "Modifying state during view update, this will cause undefined behavior." for this line of code: "needsGoBack = false". And the "needsGoBack" variable will not be set to "false". The "Go Back" button works for the first time. However, because "needsGoBack" was always "true" afterward, the child view (WebView) will not be notified (updateUIView method called) for the second time.


Solution

  • By keeping your WKWebView stored in a separate object (in this case, NavigationState) that is accessible to both your ContentView, you can access the goBack() method directly. That way, you avoid the tricky problem with trying to use a Bool to signify a one-time event, which not only doesn't work in practice (as you've found), but is also semantically a little funny to think about.

    
    class NavigationState : NSObject, ObservableObject {
        @Published var currentURL : URL?
        @Published var webView : WKWebView
        
        override init() {
            let wv = WKWebView()
            self.webView = wv
    
            super.init()
            wv.navigationDelegate = self
        }
        
        func loadRequest(_ urlRequest: URLRequest) {
            webView.load(urlRequest)
        }
    }
    
    extension NavigationState : WKNavigationDelegate {
        func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
            self.currentURL = webView.url
        }
    }
    
    struct WebView : UIViewRepresentable {
        
        @ObservedObject var navigationState : NavigationState
        
        func makeUIView(context: Context) -> WKWebView  {
            return navigationState.webView
        }
        
        func updateUIView(_ uiView: WKWebView, context: Context) {
            
        }
    }
    
    struct ContentView: View {
        @StateObject var navigationState = NavigationState()
        
        var body: some View {
            VStack {
                Text(navigationState.currentURL?.absoluteString ?? "(none)")
                WebView(navigationState: navigationState)
                    .clipped()
                HStack {
                    Button("Back") {
                        navigationState.webView.goBack()
                    }
                    Button("Forward") {
                        navigationState.webView.goForward()
                    }
                }
            }.onAppear {
                navigationState.loadRequest(URLRequest(url: URL(string: "https://www.google.com")!))
            }
        }
    }