2

Assume you have this WKWebView implementation:

import Combine
import SwiftUI
import WebKit

class WebViewData: ObservableObject {
    @Published var parsedText: NSAttributedString? = nil

    var isInit = false
    var shouldUpdateView = true
}

struct WebView: UIViewRepresentable {
    let text: String
    @ObservedObject var data: WebViewData

    func makeUIView(context: Context) -> WKWebView {
        context.coordinator.view.navigationDelegate = context.coordinator
        return context.coordinator.view
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        guard data.shouldUpdateView else {
            data.shouldUpdateView = false
            return
        }

        let html = """
            <html>
                <head>
                    <meta charset="UTF-8" />
                    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
                </head>
                <body>
                    \(text)

                <script>
                    let isScrolling = false;
                    let timer;

                    function toggleScrolling() {
                        if(!isScrolling) {
                            timer = setInterval(function() {
                                window.scrollBy(0, 1);
                            }, \(80 / autoScrollVelocity));
                        } else {
                            clearInterval(timer)
                        }

                        isScrolling = !isScrolling;
                    }
                </script>
                </body>
            </html>
        """

        uiView.loadHTMLString(html, baseURL: nil)
    }

    func makeCoordinator() -> WebViewCoordinator {
        return WebViewCoordinator(view: self)
    }
}

class WebViewCoordinator: NSObject, WKNavigationDelegate {
    let view: WebView

    init(view: WebView) {
        self.view = view

        super.init()
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        DispatchQueue.main.async {
            if !self.view.data.isInit {
                self.view.data.isInit = true
                // useless text parsing here...
            }
        }
    }
}

in this view

import SwiftUI

struct ReadingView: View {
    @ObservedObject var webViewData = WebViewData()
    private let text: String

    init(text: String?) {
        self.text = text ?? "Sorry, this reading is empty"
    }

    var body: some View {
        VStack {
            Button("Auto scroll") {
                ??????
            }
            WebView(title: self.title, text: self.text, data: self.webViewData)
        }
        .onReceive(self.webViewData.$parsedText, perform: { parsedText in
            if let parsedText = parsedText {
               print(parsedText)
            }
        })
    }
}

Now, in the button with label Auto scroll, how is it possible to call the javascript inside the html toggleScrolling() (or moving this code in a WKUserScript if necessary)? I'm pretty lost here.

Thanks in advance for any suggestion

1 Answer 1

4

I'm going to address the question itself (calling evaluateJavascript from a SwiftUI button) and not necessarily the javascript itself (your toggleScrolling function), which I haven't tested.

I think this is a great opportunity to use Combine (which means you have to make sure to import Combine at the top of your file) to pass messages between the views through the ObservableObject you have set up.

Here's the final code (I had to change a few minor things about the original that wouldn't compile):


class WebViewData: ObservableObject {
    @Published var parsedText: NSAttributedString? = nil

    var functionCaller = PassthroughSubject<Void,Never>()
    
    var isInit = false
    var shouldUpdateView = true
}

struct WebView: UIViewRepresentable {
    let text: String
    @StateObject var data: WebViewData

    func makeUIView(context: Context) -> WKWebView {
        let webview = WKWebView()
        webview.navigationDelegate = context.coordinator
        return webview
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        guard data.shouldUpdateView else {
            data.shouldUpdateView = false
            return
        }

        context.coordinator.tieFunctionCaller(data: data)
        context.coordinator.webView = uiView
        
        let html = """
            <html>
                <head>
                    <meta charset="UTF-8" />
                    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
                </head>
                <body>
                    \(text)

                <script>
                    function doAlert() { document.body.innerHTML += "hi"; }
                </script>
                </body>
            </html>
        """

        uiView.loadHTMLString(html, baseURL: nil)
    }

    func makeCoordinator() -> WebViewCoordinator {
        return WebViewCoordinator(view: self)
    }
}

class WebViewCoordinator: NSObject, WKNavigationDelegate {
    var parent: WebView
    var webView: WKWebView? = nil

    private var cancellable : AnyCancellable?
    
    init(view: WebView) {
        self.parent = view
        super.init()
    }
    
    func tieFunctionCaller(data: WebViewData) {
        cancellable = data.functionCaller.sink(receiveValue: { _ in
            self.webView?.evaluateJavaScript("doAlert()")
        })
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        DispatchQueue.main.async {
            if !self.parent.data.isInit {
                self.parent.data.isInit = true
                // useless text parsing here...
            }
        }
    }
}


struct ReadingView: View {
    @StateObject var webViewData = WebViewData()
    var text : String

    init(text: String?) {
        self.text = text ?? "Sorry, this reading is empty"
    }

    var body: some View {
        VStack {
            Button("Call javascript") {
                webViewData.functionCaller.send()
            }
            WebView(text: text, data: webViewData)
        }
        .onReceive(webViewData.$parsedText, perform: { parsedText in
            if let parsedText = parsedText {
               print(parsedText)
            }
        })
    }
}

What happens?

  1. There's a PassthroughSubject on WebViewData that doesn't take an actual value (it just takes Void) that is used to send a signal from the SwiftUI view to the WebViewCoordinator.

  2. The WebViewCoordinator subscribes to that publisher and runs evaluateJavasscript. In order to do this, it has to have a reference to the WKWebView, which you can see I pass along in updateUIView

  3. You weren't actually returning a WKWebView in makeUIView (or maybe you were but the simplified code for the question had mangled it a bit)

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

1 Comment

THANKS, THANKS, THANKS. I had to ditch StateObject in favor of ObservableObject for IOS13 compatibility. It work like a charm.

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.