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).

An Hoa
  • 1,207
  • 12
  • 38
  • 1
    Wrap `WKWebView` into UIViewRepresentable, like in https://stackoverflow.com/a/59790493/12299030. – Asperi Dec 17 '20 at 04: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 https://github.com/kylehickinson/SwiftUI-WebView to wrap WKWebView into a UIViewRepresentable.) – An Hoa Dec 17 '20 at 07:43
  • Here is alternate https://stackoverflow.com/a/60173992/12299030. You can use whichever works for you. – Asperi Dec 17 '20 at 07: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`! – An Hoa Dec 17 '20 at 08:07

2 Answers2

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

}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 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. – An Hoa Dec 17 '20 at 12:37
  • You just cannot use reference to ContentView, because it is not a reference-type, it would be a copy. – Asperi Dec 17 '20 at 12:42
  • I wasn't aware of that. I thought putting `var contentView: ContentView?` in `JSHandler` like I originally did works since `ContentView?` could be `nil`. – An Hoa Dec 18 '20 at 02:10
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)
}()
Arun
  • 222
  • 1
  • 10