Я хотел бы использовать веб-просмотр с HTML и JavaScript для рендеринга чего-либо. Ниже приведен полный код для воспроизведения проблемы. Я ожидаю, что TestViewPreview сначала отобразит текст «hello testview», а через 5 секунд отобразит «hello2 testview». Однако отображается только второй. В первом случае выдает некоторую ошибку, не уверен, связана ли она с ней:
AssessmentJavaScript: render('привет testview'); TestView.updateUIView ошибка: Error Domain=WKErrorDomain Code=4 «Исключение JavaScript произошло" UserInfo = {WKJavaScriptExceptionLineNumber=0, WKJavaScriptExceptionMessage=TypeError: undefined не является функцией, WKJavaScriptExceptionColumnNumber=0, NSLocalizedDescription=A Произошло исключение JavaScript}
Не могли бы вы взглянуть на код ниже?
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()
}
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>



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Прежде чем вызывать функцию render в JS, вам следует дождаться, пока веб-представление завершит загрузку HTML.
Вам следует использовать метод делегата didFinish в WKNavigationDelegate, чтобы определить, что загрузка веб-представления завершилась. Затем вы можете вызвать обратный вызов, который передается в TestView.
struct TestView: UIViewRepresentable {
let htmlPath: String
@Binding var state: MyState
let didLoad: () -> Void
class Coordinator: NSObject, WKNavigationDelegate {
var didLoad: () -> Void
init(didLoad: @escaping () -> Void) {
self.didLoad = didLoad
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// if the web view will load multiple pages,
// add a flag and check it here so that didLoad only gets called once
didLoad()
}
}
func makeCoordinator() -> Coordinator {
.init(didLoad: didLoad)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
if let htmlPath = Bundle.main.path(forResource: htmlPath, ofType: "html") {
do {
let htmlString = try String(contentsOfFile: htmlPath)
webView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL)
webView.scrollView.showsHorizontalScrollIndicator = true
print("TestView.makeUIView: Finished inital loading.")
} catch {
print("TestView.makeUIView: Error loading HTML file: \(error)")
}
}
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
context.coordinator.didLoad = didLoad
let script = "render('\(state.text)');"
webView.evaluateJavaScript(script) { _, error in
if let error = error {
print("TestView.updateUIView error: \(error)")
}
}
}
}
Вместо onAppear вам следует установить @State в замыкании didLoad:
@State private var state = MyState(text: "") // the initial value here is just a "dummy"
var body: some View {
TestView(htmlPath: "foo", state: self.$state) {
// set it to the initial value you want here
state.text = "hello testview"
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
state.text = "hello2 testview"
}
}
}
Обратите внимание, что ошибка, которую вы видели, все равно будет напечатана, поскольку веб-представление не загрузится при первом вызове updateUIView, но это не имеет значения. Это просто не работает render('') (начальная фиктивная пустая строка). updateUIView будет вызван снова для запуска render('hello testview') после того, как веб-представление завершит загрузку HTML.
P.S. Я надеюсь, что эта render функция — всего лишь фиктивная функция, которую вы написали для минимально воспроизводимого примера, и что ваш реальный код не такой. Вы вообще не очищаете параметр render, и это огромная уязвимость, связанная с внедрением кода.
Кроме того, мне нужно сохранить начальное значение состояния в моем реальном коде, поэтому для принятия вашего решения у меня есть state = state при его вызове. это выглядит немного глупо, поэтому мне интересно, есть ли у вас лучший обходной путь?
@ user1385809 Ну, конечно, будет задержка. Какой веб-браузер может загружать контент так же быстро, как нативное приложение? То, как вам следует очищать строку, во многом зависит от вашего фактического варианта использования, поэтому я не могу комментировать это более подробно. Установка state на самом деле просто для того, чтобы снова вызвать updateUIView, поэтому все остальное, что будет достигнуто, будет работать. Вы можете передать Bool в поле зрения и toggle при необходимости.
спасибо! код работает с небольшой задержкой по сравнению с другим содержимым в представлении. Интересно, знаете ли вы, как я могу ускорить загрузку? также спасибо за замечание. это не настоящий код, но интересно, какую проверку работоспособности мне следует выполнить?