从 SwiftUI 按钮调用 evaluateJavascript

Call evaluateJavascript from a SwiftUI button

假设您有这个 WKWebView 实现:

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

在这个视图中

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

现在,在带有标签 Auto scroll 的按钮中,如何在 html toggleScrolling() 中调用 javascript(或在 WKUserScript 中移动此代码如有必要)?我在这里迷路了。

提前感谢您的任何建议

我将解决问题本身(从 SwiftUI 按钮调用 evaluateJavascript)而不一定是 javascript 本身(您的 toggleScrolling 函数) ,我还没有测试过。

我认为这是使用 Combine(这意味着您必须确保 import Combine 在文件顶部)通过 ObservableObject 在视图之间传递消息的好机会已设置。

这是最终代码(我不得不更改一些无法编译的原始代码):


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

会发生什么?

  1. WebViewData 上有一个 PassthroughSubject 不采用实际值(它只采用 Void),用于从WebViewCoordinator.

    的 SwiftUI 视图
  2. WebViewCoordinator 订阅该发布者并运行 evaluateJavasscript。为此,它必须引用 WKWebView,您可以看到我在 updateUIView

    中传递了它
  3. 你实际上并没有在 makeUIView 中返回一个 WKWebView(或者你可能是,但是这个问题的简化代码把它弄乱了一点)