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.
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
How to Post Parameter with (+ Plus Sign) in Alamofire
Possible Bug? I Can Create Generic Instance Ignoring Constraint
How to Get Window Reference (Cgwindow, Nswindow or Windowref) from Cgwindowid in Swift
Touch Sprite, Make It Jump Up Then Fall Down Again(Repeat as Many Times as Spritenode Is Tapped.)
Filter by Day from Nsdate in Realm, Swift
Connect Physicsbodies on Tilemap in Spritekit
Cannot Use Mutating Member on Immutable Value of Type 'string'
How to Use a Protocol with a Typealias as a Func Parameter
Get Children of Children in Firebase Using Swift
Swift Xml Rss Reader... Make It Async
Swift - Nsurl Fileurlwithpath Not Unwrapped
How to Cut a Hole in a Sprite Image or Texture to Show What Is Behind It Using Spritekit in Swift
Allow Line Editing When Reading Input from The Command Line
In Swift Can You Trap "Fatal Error Unexpectedly Found Nil While Unwrapping an Optional Value"
How to Show Two Row of Text in a Menu Bar App in MAC Os
How to Decorate Siesta Request with an Asynchronous Task