Search code examples
iosweb-applicationswebviewsafariwkwebview

iOS: hash urls don't work on WKWebView, but work on Safari browser


I am trying to display a web app on an iOS WKWebView, but hash urls don't seem to work.

However if I open the same web app on iOS Safari, the hash urls work properly. They also work properly on both the Android WebView and Android Chrome.

It's very easy to verify the issue with this URL:

  • if you open it on Safari and click on the "SEARCH" button ==> it correctly displays the results page.
  • if you open it on a WKWebView and click on the "SEARCH" button ==> it incorrectly displays a blank screen.

According to this there seems to be some issues with the window.onhashchange event on WKWebView.

Is there any Swift or injected Javascript code that can fix this issue, so that the web app behaves on the WKWebView the same as on Safari?


Solution

  • I was able to get this to work by using a solution from the linked question and little bit of a hack to determine when to reload the web view with the updated URL.

    Here is a little test code that can be copied into an iOS Swift Playground.

    import UIKit
    import WebKit
    
    class WebVC: UIViewController, WKNavigationDelegate {
        var webView: WKWebView!
        var pending = false
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            webView = WKWebView()
            webView.navigationDelegate = self
            webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil)
    
            view.addSubview(webView)
            webView.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
                webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
                webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
                webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            ])
        }
    
        // Code from the linked answer
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
            if object as AnyObject? === webView && keyPath == "URL" {
                print(webView.url)
                if let url = webView.url, pending {
                    // Probably need to add a sanity check to avoid a reload loop.
                    // Not sure if that can happen in some corner case or not.
                    let req = URLRequest(url: url)
                    webView.load(req)
                }
            }
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            let req = URLRequest(url: URL(string: "https://www.lefrecce.it/Channels.Website.WEB/#/white-label/MINISITI/?minisitiCallBackId=54276&isRoundTrip=false&lang=en&departureStation=Salerno&arrivalStation=Caserta")!)
            webView.load(req)
        }
    
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences) async -> (WKNavigationActionPolicy, WKWebpagePreferences) {
            print("decidePolicyFor action, prefs: \(preferences)")
    
            pending = true
            return (.allow, preferences)
        }
    
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            print("didFinish \(navigation)")
    
            pending = false
        }
    
        func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
            print("Error: \(error)")
        }
    
        func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
            print("Provisional error: \(error)")
        }
    }
    
    PlaygroundPage.current.liveView = WebVC()
    

    The whole pending flag is a first attempt to a solution. It's likely a little fragile but it works in this case. There might be corner cases where it ends up causing a loop of reloads so further testing is needed to be sure.

    Code also needs to be added to remove the observer from webView. Probably in a deinit for the view controller.