SwiftUI WebView首次加载不显示内容问题求助
我想用WebView结合HTML与JavaScript渲染内容,以下是复现问题的完整代码。预期TestViewPreview先渲染文本“hello testview”,5秒后渲染“hello2 testview”,但只有第二个内容正常显示,首次加载出现如下错误:
evaluateJavaScript: render('hello testview'); TestView.updateUIView
error: Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=0, WKJavaScriptExceptionMessage=TypeError: undefined is not a function, WKJavaScriptExceptionColumnNumber=0, NSLocalizedDescription=A JavaScript exception occurred}
提供的代码
Swift代码
struct MyState { var text: String = "abc" } struct TestView: UIViewRepresentable { let htmlPath: String @Binding var state: MyState func makeUIView(context: Context) -> WKWebView { if let htmlPath = Bundle.main.path(forResource: htmlPath, ofType: "html") { do { let htmlString = try String(contentsOfFile: htmlPath) WebViewHelper.webView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL) WebViewHelper.webView.scrollView.showsHorizontalScrollIndicator = true print("TestView.makeUIView: Finished inital loading.") } catch { print("TestView.makeUIView: Error loading HTML file: \(error)") } } return WebViewHelper.webView } func updateUIView(_ webView: WKWebView, context: Context) { let script = "render('\(state.text)');" print("evaluateJavaScript: \(script)") webView.evaluateJavaScript(script) { _, error in if let error = error { print("TestView.updateUIView error: \(error)") } } } } struct TestViewPreview: View { @State private var state = MyState(text: "hello testview") var body: some View { TestView(htmlPath: "test_html", state: self.$state) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { state.text = "hello2 testview" } } } } #Preview { TestViewPreview() }
HTML代码(test_html.html)
<!DOCTYPE html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <div id="output"></div> <script> function render(text) { document.getElementById("output").innerHTML = text } </script> </body> </html>
问题根源
- HTML未加载完成就执行JS:
updateUIView会在makeUIView返回后立即触发,但此时WebView的HTML还未加载完毕,render函数尚未定义,调用时自然会报“undefined is not a function”错误。 - 全局WebView单例风险:使用
WebViewHelper.webView全局单例会导致视图生命周期混乱,多个TestView实例共用同一个WebView,易引发不可预期的行为。 - 字符串转义缺失:如果
state.text包含单引号,直接拼接JS脚本会破坏语法结构,导致执行失败。
修复方案
1. 等待HTML加载完成后执行JS
通过WKNavigationDelegate监听页面加载状态,仅在页面加载完成后调用render函数:
修改后的TestView代码:
struct MyState { var text: String = "abc" } struct TestView: UIViewRepresentable { let htmlPath: String @Binding var state: MyState func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, WKNavigationDelegate { var parent: TestView init(_ parent: TestView) { self.parent = parent } // 页面加载完成回调 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { parent.updateWebViewContent(webView) } } func makeUIView(context: Context) -> WKWebView { // 为每个TestView创建独立的WebView实例,避免单例冲突 let webView = WKWebView() webView.navigationDelegate = context.coordinator webView.scrollView.showsHorizontalScrollIndicator = true if let htmlPath = Bundle.main.path(forResource: htmlPath, ofType: "html") { do { let htmlString = try String(contentsOfFile: htmlPath) webView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL) print("TestView.makeUIView: 完成初始加载.") } catch { print("TestView.makeUIView: 加载HTML文件出错: \(error)") } } return webView } func updateUIView(_ webView: WKWebView, context: Context) { // 页面加载中则暂存状态,加载完成后自动更新;否则立即更新 if webView.isLoading { context.coordinator.parent.state = state } else { updateWebViewContent(webView) } } private func updateWebViewContent(_ webView: WKWebView) { // 转义单引号,避免JS语法错误 let escapedText = state.text.replacingOccurrences(of: "'", with: "\\'") let script = "render('\(escapedText)');" print("evaluateJavaScript: \(script)") webView.evaluateJavaScript(script) { _, error in if let error = error { print("TestView.updateUIView 错误: \(error)") } } } } struct TestViewPreview: View { @State private var state = MyState(text: "hello testview") var body: some View { TestView(htmlPath: "test_html", state: self.$state) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { state.text = "hello2 testview" } } } } #Preview { TestViewPreview() }
2. 移除全局WebView单例
每个TestView实例创建独立的WKWebView,确保视图生命周期独立,避免资源冲突。
3. 处理字符串转义
通过replacingOccurrences(of: "'", with: "\\'")转义文本中的单引号,保证JS脚本语法正确。
4. 验证HTML文件
确保test_html.html已正确添加到项目中,且在Build Phases的Copy Bundle Resources列表内,保证Bundle能正确读取该文件。
修复后,首次加载时WebView会等待HTML加载完成,执行render显示“hello testview”;5秒后状态更新,再次执行render显示“hello2 testview”,不会再出现首次加载的错误。
内容的提问来源于stack exchange,提问作者user1385809

