Hyperlinks open in Web browser instead of embedded WKWebView using Swift5/SwiftUI

Good morning!

I am developing an iOS app that gets its data from an API and displays it in a list. The list item view has NavigationLink embedded in it that sends users to a detail view. In this detail view, I have some Text views but the issue I am running into is a WKWebView that I have implemented to display some HTML that's is retrieved from the API call.

The WKWebView displays the HTML just how I want it to. The issue I have is in the HyperLinks that are displayed in the WKWebView. When a user taps on a link, it opens inside of the web view. Instead of opening in the web view, I would like this link to open in the user's default web browser. I have searched and found ways of doing this in older versions of Swift using classes but my web view is initialized inside of a struct that conforms to the UIViewRepresentable protocol.

I don't know how to get links to open in the browser instead of the WebView so any help would be appreciated. Here is the code for my WebView that is being used on the details page.

struct NewsItemWebView: UIViewRepresentable {
// HTML from API Call
    var text: String
// Method to create the View
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }
// Method to update the View by changing properties of the WebView
    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.isOpaque = false
        uiView.backgroundColor = UIColor.white
        uiView.loadHTMLString(text, baseURL: nil)
    }
}

Here is how I am implementing the WebView on DetailView

NewsItemWebView(text: item.PageContent)
    .frame(height: 450)

Any help on how I can make links open in a browser would be great. Thanks in advance.

Accepted Reply

You simply need to use the WKNavigationDelegate to detect when there has been a navigation request (i.e. clicked on a link). You will need a class for this and that is where UIViewRepresentable.Coordinator comes in.

You can implement it like this:

struct NewsItemWebView: UIViewRepresentable {
    var text: String

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        ...
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
}

extension ItemWebView {
    @MainActor class Coordinator: NSObject, WKNavigationDelegate {
        // used the async method so you don't forget to call a completion handler
        // you can still use the completion handler method
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
            if let url = navigationAction.request.url, /* should `url` be opened in Safari */, await UIApplication.shared.open(url) {
                return .cancel
            } else {
                return .allow
            }
        }
    }
}

You will need to check the navigation request's url to see if it should be opened in Safari. This will prevent opening the originally loaded content in Safari instead of the app.

  • Thanks BabyJ for the info! I managed to make it work using the code sample you provided. Now I am going to dissect it to I know how each part works and how they work together to solve the task I was trying to solve. Much appreciated!

  • Won't UIApplication#open actually open the other app?

Add a Comment

Replies

You simply need to use the WKNavigationDelegate to detect when there has been a navigation request (i.e. clicked on a link). You will need a class for this and that is where UIViewRepresentable.Coordinator comes in.

You can implement it like this:

struct NewsItemWebView: UIViewRepresentable {
    var text: String

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        ...
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
}

extension ItemWebView {
    @MainActor class Coordinator: NSObject, WKNavigationDelegate {
        // used the async method so you don't forget to call a completion handler
        // you can still use the completion handler method
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
            if let url = navigationAction.request.url, /* should `url` be opened in Safari */, await UIApplication.shared.open(url) {
                return .cancel
            } else {
                return .allow
            }
        }
    }
}

You will need to check the navigation request's url to see if it should be opened in Safari. This will prevent opening the originally loaded content in Safari instead of the app.

  • Thanks BabyJ for the info! I managed to make it work using the code sample you provided. Now I am going to dissect it to I know how each part works and how they work together to solve the task I was trying to solve. Much appreciated!

  • Won't UIApplication#open actually open the other app?

Add a Comment

BabtJ's solution worked well for me too. One tiny detail: the line beginning if let url = navigationAction.request.url has one too many comm

  • If you mean comma, then no it doesn't. The commented part should be a boolean value of whether the URL should be opened in Safari. It's probably not clear enough in the sample code.

Add a Comment

Am I correct that extension ItemWebView should be extension NewsItemWebView in the accepted reply?