Wkwebview - Update HTML Tags from Swiftui Textfields

WKWebView - Update HTML Tags from SwiftUI TextFields

Here is full module of fixed variant that works with Xcode 13.2 / iOS 15.2. Fixed coordinator, of course you should always use it from context, as well several injections and communication between entities.

demo

Note: see also for comments inline

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

Change ObservedObject Value with Wkwebview

Here I see cycle loading by updating Myscore which result in updateUIView call and so forth...

Initial loading should be placed into makeUIView

func makeUIView(context: Context) -> WKWebView {
let coordinator = makeCoordinator()
let userContentController = WKUserContentController()
userContentController.add(coordinator, name: "bridge")

let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController

let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
_wkwebview.navigationDelegate = coordinator

// make here initial loading !!!
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)


return _wkwebview
}

func updateUIView(_ webView: WKWebView, context: Context) {
// reload should be made here only if base url changed
// externally
}

Tested with Xcode 13.4 / iOS 15.5

Send update to child view (WKWebView) using swiftUI?

By keeping your WKWebView stored in a separate object (in this case, NavigationState) that is accessible to both your ContentView, you can access the goBack() method directly. That way, you avoid the tricky problem with trying to use a Bool to signify a one-time event, which not only doesn't work in practice (as you've found), but is also semantically a little funny to think about.


class NavigationState : NSObject, ObservableObject {
@Published var currentURL : URL?
@Published var webView : WKWebView

override init() {
let wv = WKWebView()
self.webView = wv

super.init()
wv.navigationDelegate = self
}

func loadRequest(_ urlRequest: URLRequest) {
webView.load(urlRequest)
}
}

extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.currentURL = webView.url
}
}

struct WebView : UIViewRepresentable {

@ObservedObject var navigationState : NavigationState

func makeUIView(context: Context) -> WKWebView {
return navigationState.webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {

}
}

struct ContentView: View {
@StateObject var navigationState = NavigationState()

var body: some View {
VStack {
Text(navigationState.currentURL?.absoluteString ?? "(none)")
WebView(navigationState: navigationState)
.clipped()
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
}
}.onAppear {
navigationState.loadRequest(URLRequest(url: URL(string: "https://www.google.com")!))
}
}
}

WKWebView - evaluate JavaScript without seeing changes

This looks like Apple's defect, because updateUIView is called, but evaluateJavaScript (internal JavaScript) generates exception, because bridge.onMessage is lost and not a function more.

Here is found work-around - reset WebView on scene activation (app comes into foreground), as it recreated with current states the previous values are preserved. Tested with 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 !!
}
}
}
}
}
}

SwiftUI coordinator not updating the containing view's property

Here is your code, a bit modified for demo, with used view model instance of ObservableObject holding your loading state.

import SwiftUI
import WebKit
import Combine

class WebViewModel: ObservableObject {
@Published var link: String
@Published var didFinishLoading: Bool = false

init (link: String) {
self.link = link
}
}

struct WebView: UIViewRepresentable {
@ObservedObject var viewModel: WebViewModel

let webView = WKWebView()

func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}

func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
return
}

class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel

init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("WebView: navigation finished")
self.viewModel.didFinishLoading = true
}
}

func makeCoordinator() -> WebView.Coordinator {
Coordinator(viewModel)
}

}

struct WebViewContentView: View {
@ObservedObject var model = WebViewModel(link: "https://apple.com")

var body: some View {
VStack {
TextField("", text: $model.link)
WebView(viewModel: model)
if model.didFinishLoading {
Text("Finished loading")
.foregroundColor(Color.red)
}
}
}
}

struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebViewContentView()
}
}


Related Topics



Leave a reply



Submit