2

I want to have a WKWebView with JavaScript handling in SwiftUI. From Swift Variables Initialization, I am doing the following: (I am using https://github.com/kylehickinson/SwiftUI-WebView to provide a wrapper for WKWebView in SwiftUI that also add valuable layout constraints.)

struct ContentView: View {

    var body: some View {
        WebView(webView: myWebView)
        .onAppear {
            let url = Bundle.main.url(forResource: "index", withExtension: "html")!
            myWebView.loadFileURL(url, allowingReadAccessTo: url)
            let request = URLRequest(url: url)
            myWebView.load(request)
        }
    }

    class JSHandler : NSObject, WKScriptMessageHandler {
        var contentView: ContentView?

        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            print("documentReady")
            contentView!.myWebView.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
            }
        }
    }

    let myJSHandler = JSHandler()

    var myWebView: WKWebView = {
        let config = WKWebViewConfiguration()
        let controller = WKUserContentController()
        controller.add(myJSHandler , name: "documentReady")
        config.userContentController = controller
        return WKWebView(frame: .zero, configuration: config)
    }()
}

But then I learned from Instance member cannot be used on type that this doesn't work because the closure doesn't have reference to self. I need the WKWebView to have dedicated config object so I can't just use the other constructor. I need a reference to it to do evaluateJavaScript.

How to make this work?

EDIT 1: Add the body and mention the framework used to wrap WKWebView.

EDIT 2: Added code to clarify that I need two way communications from WKWebView to native app via WKScriptMessageHandler (to get notified when the HTML document is ready) and from native app to WKWebView via evaluateJavaScript (to transfer data upon the HTML document is ready).

4
  • 1
    Wrap WKWebView into UIViewRepresentable, like in stackoverflow.com/a/59790493/12299030. Commented Dec 17, 2020 at 4:28
  • @Asperi In the struct WebView, you are using var webview: WKWebView = WKWebView() but I need to use the constructor with custom configuration. Wouldn't I encounter the same problem then? (I already use github.com/kylehickinson/SwiftUI-WebView to wrap WKWebView into a UIViewRepresentable.) Commented Dec 17, 2020 at 7:43
  • Here is alternate stackoverflow.com/a/60173992/12299030. You can use whichever works for you. Commented Dec 17, 2020 at 7:50
  • @Asperi Thanks for the solution. I would like to point out that since the configuration was done inside the wrapper so if I want to make another WKWebView with a different set of JS methods, I would have to make another wrapper! Another issue is that I need a reference to the WKWebView in my ContentView so that I can call evaluateJavaScript! Commented Dec 17, 2020 at 8:07

2 Answers 2

1

One possible solution is to configure WKWebView in init

struct ContentView: View {
    private var myWebView: WKWebView

    init() {
        let config = WKWebViewConfiguration()
        let controller = WKUserContentController()
        controller.add(myJSHandler , name: "documentReady")
        config.userContentController = controller
        myWebView = WKWebView(frame: .zero, configuration: config)
    }

    var body: some View {
        WebView(webView: myWebView)
        .onAppear {
            let url = Bundle.main.url(forResource: "index", withExtension: "html")!
            myWebView.loadFileURL(url, allowingReadAccessTo: url)
            let request = URLRequest(url: url)
            myWebView.load(request)
        }
    }

    class JSHandler : NSObject, WKScriptMessageHandler {

        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            print("documentReady")
            message.webView?.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
            }
        }
    }

    let myJSHandler = JSHandler()

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

3 Comments

I have just came up with a solution by moving myWebView inside JSHandler but then I don't know what to name JSHandler as it not only handles JavaScript callbacks but also serves as a wrapper for the WKWebView. This is much better. Thanks for the tip of using message.webView? instead of using a reference to ContentView by the way.
You just cannot use reference to ContentView, because it is not a reference-type, it would be a copy.
I wasn't aware of that. I thought putting var contentView: ContentView? in JSHandler like I originally did works since ContentView? could be nil.
0

Just make myWebView a lazy variable, This makes sure that the jsHandler is initialised before WebView.

lazy var myWebView: WKWebView = {
    let config = WKWebViewConfiguration()
    let controller = WKUserContentController()
    controller.add(myJSHandler , name: "documentReady")
    config.userContentController = controller
    return WKWebView(frame: .zero, configuration: config)
}()

1 Comment

This won't work for SwiftUI view, as it has not modifiable self.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.