2

So I've wrapped WKWebView in an UIViewRepresentable and built a coordinator in order to access its navigation delegate's functions. In the webView(_:didFinish:) function I am trying to update the view's didFinishLoading variable. If I print right after assigning, it prints true - the expected behavior. But, in the parent view, when I call the getHTML function, it prints false - even if I wait until the WKWebView is fully loaded. Here is the code:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    @Binding var link: String

    init(link: Binding<String>) {
        self._link = link
    }

    private var didFinishLoading: Bool = false

    let webView = WKWebView()

    func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
        self.webView.load(URLRequest(url: URL(string: self.link)!))
        self.webView.navigationDelegate = context.coordinator
        return self.webView
    }

    func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
        return
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        private var webView: WebView

        init(_ webView: WebView) {
            self.webView = webView
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            print("WebView: navigation finished")
            self.webView.didFinishLoading = true
        }
    }

    func makeCoordinator() -> WebView.Coordinator {
        Coordinator(self)
    }

    func getHTML(completionHandler: @escaping (Any?) -> ()) {
        print(self.didFinishLoading)
        if (self.didFinishLoading) {
            self.webView.evaluateJavaScript(
                """
                document.documentElement.outerHTML.toString()
                """
            ) { html, error in
                    if error != nil {
                        print("WebView error: \(error!)")
                        completionHandler(nil)
                    } else {
                        completionHandler(html)
                    }
            }
        }
    }
}

struct WebView_Previews: PreviewProvider {
    @State static var link = "https://apple.com"
    static var previews: some View {
        WebView(link: $link)
    }
}

Toma
  • 2,764
  • 4
  • 25
  • 44
  • Your WebView is struct, so value, so it is copied in your Coordinator. – Asperi Nov 12 '19 at 16:50
  • @Asperi oh, right... forgot I am in a struct. Is there any way I can pass it by reference? `inout` does not apply here since it would make `makeCoordinator` mutating and thus it won't conform to the `UIViewRepresentable` protocol – Toma Nov 12 '19 at 18:08
  • In general I would tend to recommend view model as subclass of ObservableObject from Combine and pass its instance here and there between SwifthUI structs as reference. – Asperi Nov 12 '19 at 18:31
  • @Asperi but it would still make the function mutable – Toma Nov 12 '19 at 19:04
  • See below in code what I was about – Asperi Nov 12 '19 at 19:46

1 Answers1

8

Here is your code, a bit modified for demo, with used view model instance of ObservableObject holding your loading state.

import SwiftUI
import WebKit
import Combine

class WebViewModel: ObservableObject {
    @Published var link: String
    @Published var didFinishLoading: Bool = false
    
    init (link: String) {
        self.link = link
    }
}

struct WebView: UIViewRepresentable {
    @ObservedObject var viewModel: WebViewModel

    let webView = WKWebView()

    func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
        self.webView.navigationDelegate = context.coordinator
        if let url = URL(string: viewModel.link) {
            self.webView.load(URLRequest(url: url))
        }
        return self.webView
    }

    func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
        return
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        private var viewModel: WebViewModel

        init(_ viewModel: WebViewModel) {
            self.viewModel = viewModel
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            print("WebView: navigation finished")
            self.viewModel.didFinishLoading = true
        }
    }

    func makeCoordinator() -> WebView.Coordinator {
        Coordinator(viewModel)
    }

}

struct WebViewContentView: View {
    @ObservedObject var model = WebViewModel(link: "https://apple.com")

    var body: some View {
        VStack {
            TextField("", text: $model.link)
            WebView(viewModel: model)
            if model.didFinishLoading {
                Text("Finished loading")
                    .foregroundColor(Color.red)
            }
        }
    }
}

struct WebView_Previews: PreviewProvider {
    static var previews: some View {
        WebViewContentView()
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • How can I use `@Binding` vars now if they are `@Published` now? – Toma Nov 12 '19 at 20:07
  • Using $ to @Published property. I updated example with TextField used binding to link – Asperi Nov 13 '19 at 03:59
  • Thank you. One last question. Is there any way to use `@State` with the ViewModel? I'd like the WebView to re-render when the parent's `link` property is changed – Toma Nov 13 '19 at 05:55
  • 2
    `@State` is designed to be used within View, so if you want to place `@State` into ViewModel, then answer is no. But you can have some `@State` inside View and assign values to it from ViewModel, which can be shared between, say, similar views. – Asperi Nov 13 '19 at 05:59
  • @Asperi, _But you can have some @State inside View and assign values to it from ViewModel_ - not necessarily. `@State` & `@ObservedObject` work similarly. Both of them will perform a UI refresh if mutation happens. – nayem Nov 13 '19 at 08:57
  • @Asperi I encountered a similar problem and you answer is good!. But I found some bug: if I run this code in preview - all work perfect. If I run on device - it is not workin! =(. How that possible? Maybe you have some answer. Tnx! – Владимир Михайлов Jun 25 '20 at 14:22
  • I noticed one strange thing. Setting value for `viewModel` property in`WKNavigationDelegate ` methods cause an invocation`updateUIView ` method of`UIViewRepresentable ` protocol. Is that normal? – Nikolai Nagornyi Feb 02 '23 at 16:58