I am attempting to show when a web page does not load, for example when I enter an invalid address such as
htps://ww.mybadexample.com
Here the code:
struct WebTestView: UIViewRepresentable {
typealias UIViewType = WKWebView
let url: String
func makeCoordinator() -> WebTestView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: url) else {
return WKWebView()
}
let webView = WKWebView()
let request = URLRequest(url: url)
webView.navigationDelegate = context.coordinator
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
extension WebTestView {
class Coordinator: NSObject, WKNavigationDelegate {
private let parent: WebTestView
init(_ parent: WebTestView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Failed loading:", error)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finished loading")
}
}
}
For some reason the following fuction is never called:
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)
When I enter a correct url, webView(_, didFinish) is called, so the coordinator seems to be working correctly.
What am I missing? Am I using an incorrect method? Any pointers?
Thanks!
While learning to use webview in my app I just added a webview and detected a memory leak.
I found a lot of demos on the Internet and tested them and all have this problem.
Here is my test code:
Instruments screenshot
import SwiftUI
import WebKit
struct SWKWebView: UIViewRepresentable {
#Binding var url: String?
func makeUIView(context: Context) -> WKWebView {
let webview = WKWebView()
webview.navigationDelegate = context.coordinator
return webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let url = url, let requetURL = URL(string: url) {
uiView.load(URLRequest(url: requetURL))
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject,WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.title") { (result, error) in
print("didFinish:\(String(describing: result ?? ""))")
}
}
}
}
struct TTTest: View {
#State var url: String? = "https://www.google.com"
var body: some View {
SWKWebView(url: $url)
}
}
When I updated the IOS system to version 15.5, this problem was solved
I am trying to call a function in my javascript file using evaluateJavaScript from webkit in swiftui. But it seems that that function is never called and it throws error:
Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=1, WKJavaScriptExceptionMessage=ReferenceError: Can't find variable: retrieveChartData, WKJavaScriptExceptionColumnNumber=18, WKJavaScriptExceptionSourceURL=undefined, NSLocalizedDescription=A JavaScript exception occurred}
My code is :
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: URL
var ticker: String
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
return view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
print("send ticker \(ticker)")
uiView.evaluateJavaScript("retrieveChartData(\(ticker))") { result, error in
guard error == nil else {
print(error!)
return
}
}
print("loading")
uiView.load(URLRequest(url: url))
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("send ticker \(ticker)")
webView.evaluateJavaScript("retrieveChartData(\(ticker))") { result, error in
guard error == nil else {
print(error!)
return
}
}
}
}
Any suggestion?
I am building a view that uses WKWebView. To that end I am using UIViewRepresentable.
I would like to show the web page loading progress using a ProgressView. To that end I want to drive the progress UI using the WKWebView.estimatedProgress.
I am putting here the entire code. If you copy and paste this in a project you'll see that TestWContainer is stuck updating. I am trying to understand how to fix this, and I guess understanding the correct design pattern to follow in a situation like this to avoid endless view updates.
Here the code:
struct TestWContainer: View {
#State var url:URL?
#State var userSetUrl:URL?
#State var showLoader:Bool?
#State var estimatedProgress:Double?
var body: some View {
ZStack {
WebView(currentURL: $url, userSetURL: $userSetUrl, showLoader: $showLoader, estimatedProgress: $estimatedProgress)
if let estimatedProgress = estimatedProgress {
if estimatedProgress > 0 && estimatedProgress < 1 {
let _ = print("estimatedPogress: \(estimatedProgress)")
VStack(spacing:0) {
ProgressView(value: estimatedProgress, total: 1)
.frame(height: 3)
Spacer()
}
}
}
}
}
}
struct WebView: UIViewRepresentable {
#Binding var currentURL:URL?
#Binding var userSetURL:URL?
#Binding var showLoader:Bool?
#Binding var estimatedProgress:Double?
fileprivate let defaultURL:URL = URL(string: "https://www.google.com")!
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ webView: WebView) {
self.parent = webView
}
deinit {
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.showLoader = false
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.showLoader = false
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.showLoader = true
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
dispatchPrecondition(condition: .onQueue(.main))
print("Observing keyPath: \(keyPath). change: \(change). object: \(object)")
guard let wv = object as? WKWebView else { return }
if keyPath == #keyPath(WKWebView.estimatedProgress) {
print("O: progress: \(wv.estimatedProgress)")
DispatchQueue.main.async {
self.parent.estimatedProgress = wv.estimatedProgress
}
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
print("setup: \(currentURL)")
load(url: userSetURL, in: webView)
return webView
}
fileprivate func load(url:URL?, in webView:WKWebView) {
if let url = url {
print("load url....: \(url)")
let req = URLRequest(url: url)
webView.load(req)
} else {
print("load url google case...")
let req = URLRequest(url: defaultURL)
webView.load(req)
}
}
func updateUIView(_ webView: WKWebView, context: Context) {
print("updateUIView: \(userSetURL)")
load(url: userSetURL, in: webView)
}
}
How can I change things so that I can drive a ProgressView with the built in WKWebView estimatedProgress property without getting stuck in a View update cycle?
You have a circular dependency -- you've defined estimatedProgress as #State and then sent it to the WebView as a #Binding. The WebView updates estimatedProgress, which then re-renders the view (since the state is updated). In WebView, you're calling load in updateUIView which is called every time the WebView re-renders with new input (ie one of its Bindings has changed).
The easiest fix is to just remove the load call from updateUIView. But, that would have the side effect of now updating the WebView in the case that you wanted to change the URL.
Another option is to store the state in an ObservableObject and only pass the trait that necessitates an update (I assume userSetURL) to the WebView:
class WebViewState : ObservableObject {
#Published var url:URL?
#Published var userSetUrl:URL?
#Published var showLoader:Bool?
#Published var estimatedProgress:Double?
}
struct TestWContainer: View {
#StateObject var webViewState = WebViewState()
var body: some View {
ZStack {
WebView(webViewState : webViewState, userSetURL: webViewState.userSetUrl)
if let estimatedProgress = webViewState.estimatedProgress {
if estimatedProgress > 0 && estimatedProgress < 1 {
let _ = print("estimatedPogress: \(estimatedProgress)")
VStack(spacing:0) {
ProgressView(value: estimatedProgress, total: 1)
.frame(height: 3)
Spacer()
}
}
}
}
}
}
struct WebView: UIViewRepresentable {
var webViewState : WebViewState
var userSetURL: URL?
fileprivate let defaultURL:URL = URL(string: "https://www.google.com")!
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ webView: WebView) {
self.parent = webView
}
deinit {
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.webViewState.showLoader = false
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.webViewState.showLoader = false
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.webViewState.showLoader = true
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
dispatchPrecondition(condition: .onQueue(.main))
print("Observing keyPath: \(keyPath). change: \(change). object: \(object)")
guard let wv = object as? WKWebView else { return }
if keyPath == #keyPath(WKWebView.estimatedProgress) {
print("O: progress: \(wv.estimatedProgress)")
DispatchQueue.main.async {
self.parent.webViewState.estimatedProgress = wv.estimatedProgress
}
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
print("setup: \(webViewState.url)")
load(url: userSetURL, in: webView)
return webView
}
fileprivate func load(url:URL?, in webView:WKWebView) {
if let url = url {
print("load url....: \(url)")
let req = URLRequest(url: url)
webView.load(req)
} else {
print("load url google case...")
let req = URLRequest(url: defaultURL)
webView.load(req)
}
}
func updateUIView(_ webView: WKWebView, context: Context) {
print("updateUIView: \(userSetURL)")
load(url: userSetURL, in: webView)
}
}
You could also use a similar strategy with Combine to watch an updated property from within the WebView or it's coordinator, but that may be overkill for this situation.
I have another questions with WKWebView.
The following View is called with an request parameter which defines the complete url:
struct TrainerTab: UIViewRepresentable {
let request: String
private let hostingUrl: String = "https://page.tld"
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let url = "\(hostingUrl)/\(request)"
uiView.load(URLRequest(url: URL(string: url)!))
}
}
The HTML-Files from the page.tld have some href-Links.
I would like to open them in Safari. I read, that I have to implement the delegate method that gets called when the user taps a link.
But how can I do this?
You can use a Coordinator in your UIViewRepresentatable to act as a navigation delegate for the WKWebView:
struct TrainerTab: UIViewRepresentable {
let request: String
private let hostingUrl: String = "https://page.tld"
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.navigationDelegate = context.coordinator
let url = "\(hostingUrl)/\(request)"
uiView.load(URLRequest(url: URL(string: url)!))
}
class Coordinator : NSObject, WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
}
//make some conditions based on the URL here
if myCondition {
decisionHandler(.cancel)
UIApplication.shared.open(url)
} else {
decisionHandler(.allow)
}
}
}
}