WKWebView — оценивайте JavaScript, не видя изменений
В моем приложении SwiftUI я использовал WKWebView для обновления некоторых html-тегов с собственной стороны, используя SwiftUI TextFields. Чтобы связь работала, я использую
evaluateJavaScript()
метод отправки данных из нативного в веб-просмотр.
Мой код выглядит так:
struct DemoView: View {
@State private var headline: String = "Initial"
var body: some View {
NavigationView {
VStack {
Form {
TextField("Your headline", text: $headline)
}
WebView(headline: $headline)
}
}
}
}
import WebKit
let bridgeHTML = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, viewport-fit=cover">
</head>
<body>
<h3 id="headline">Headline</h3>
<script>
// to receive messages from native
webkit.messageHandlers.bridge.onMessage = (msg) => {
document.getElementById("headline").textContent = msg
}
</script>
</body>
</html>
"""
struct WebView: UIViewRepresentable {
@Binding var headline: String
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}
private var owner: WebView
init(owner: WebView) {
self.owner = owner
}
var webView: WKWebView?
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView = webView
self.messageToWebview(msg: self.owner.headline) // initial value loading !!
}
func messageToWebview(msg: String) {
self.webView?.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(msg)')")
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(owner: self)
}
func makeUIView(context: Context) -> WKWebView {
let userContentController = WKUserContentController()
userContentController.add(context.coordinator, name: "bridge")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
_wkwebview.navigationDelegate = context.coordinator
guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") else { return _wkwebview }
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
_wkwebview.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
// _wkwebview.loadHTMLString(bridgeHTML, baseURL: nil) // << used for testing
return _wkwebview
}
func updateUIView(_ webView: WKWebView, context: Context) {
// this works for update, but for initial it is too early !!
webView.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(headline)')")
}
}
Он работает, внутри
didFinish()
метод делегата, я устанавливаю начальные значения для отображения на веб-странице. И когда я что-то набираю, оно автоматически обновляется на веб-странице. пока все хорошо, но если я ничего не делаю в течение более длительного времени (около 1 минуты) или если я переключаю приложение (не закрывая его) и возвращаюсь к приложению, а затем что-то меняю в текстовом поле, веб-страница больше не обновляется автоматически.
Я мог бы, конечно, бежать
webView.reload()
, но тогда я теряю свое предыдущее состояние. Вместо
Initial
Я получаю заголовок. Логично, потому что это было установлено внутри тега h3, но этого я хочу избежать.
Итак, как я могу убедиться, что всякий раз, когда
evaluteJavascript()
вызывается, веб-страница также обновляется без потери предыдущего состояния, независимо от того, как долго я жду или переключаюсь между приложениями? Есть ли способ решить эту проблему?
1 ответ
Это выглядит как дефект Apple, потому что вызывается updateUIView, но
evaluateJavaScript
(внутренний JavaScript) генерирует исключение, потому что bridge.onMessage потерян, а не функция больше.
Здесь найден обходной путь - сбросить WebView при активации сцены (приложение выходит на передний план), так как оно воссоздано с текущими состояниями, предыдущие значения сохраняются. Протестировано с Xcode 13.2/iOS 15.2
struct DemoView: View {
@Environment(\.scenePhase) var scenePhase
@State private var headline: String = "Initial"
@State private var reset = false
var body: some View {
NavigationView {
VStack {
Form {
TextField("Your headline", text: $headline)
}
WebView(headline: $headline).id(reset) // << here !!
.onChange(of: scenePhase) {
if case .active = $0 {
self.reset.toggle() // << here !!
}
}
}
}
}
}