307

I have a very simple UIWebView with content from my application bundle. I would like any links in the web view to open in Safari instead of in the web view. Is this possible?

Wayne Chen
  • 305
  • 2
  • 15
David Beck
  • 10,099
  • 5
  • 51
  • 88

10 Answers10

660

Add this to the UIWebView delegate:

(edited to check for navigation type. you could also pass through file:// requests which would be relative links)

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if (navigationType == UIWebViewNavigationTypeLinkClicked ) {
        [[UIApplication sharedApplication] openURL:[request URL]];
        return NO;
    }

    return YES;
}

Swift Version:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        if navigationType == UIWebViewNavigationType.LinkClicked {
            UIApplication.sharedApplication().openURL(request.URL!)
            return false
        }
        return true
    }

Swift 3 version:

func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if navigationType == UIWebViewNavigationType.linkClicked {
        UIApplication.shared.openURL(request.url!)
        return false
    }
    return true
}

Swift 4 version:

func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
    guard let url = request.url, navigationType == .linkClicked else { return true }
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
    return false
}

Update

As openURL has been deprecated in iOS 10:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
        if (navigationType == UIWebViewNavigationTypeLinkClicked ) {
            UIApplication *application = [UIApplication sharedApplication];
            [application openURL:[request URL] options:@{} completionHandler:nil];
            return NO;
        }

        return YES;
}
Vlad
  • 7,199
  • 2
  • 25
  • 32
drawnonward
  • 53,459
  • 16
  • 107
  • 112
  • drawnonward, would you mind updating your answer with reference to the answer and comments below. – Toby Allen Mar 23 '11 at 22:08
  • How to go back to the application once the user closes the browser? – Johnny Everson May 28 '11 at 16:14
  • 3
    @ Jhonny Everson: You have no control over what happens after any external app (including Safari) is closed. If you want to get back to your app when the user is done browsing, don't open up Safari, just use the UIWwbView and a "Done"-button. – geon Jul 08 '11 at 16:03
  • 1
    Worked like a charm with local HTML file. – necixy Mar 20 '12 at 09:52
  • This will let you not handle Addresses and will open Maps.app by default `NSString *urlString = inRequest.URL.absoluteString; if ([urlString rangeOfString:@"x-apple-data-detectors"].location != NSNotFound) { return YES; }` – Dex Nov 13 '12 at 10:24
  • 1
    I think, in Swift, a `switch` is preferable for enum types – SDJMcHattie Feb 17 '16 at 10:22
  • @KPM below makes a good point about filtering the URL hostname for anchor links. I think scheme-parsing would be a better approach, and one that could be expanded to support app-specific/custom schemes. – MandisaW Feb 29 '16 at 20:41
  • **WARNING:** This does not work if the page request comes from Javascript. Please see my answer below. – IanS Jun 29 '16 at 14:46
  • doesn't work with YouTube site https://stackoverflow.com/questions/55064055/playing-a-youtube-video-in-webview – user25 Mar 08 '19 at 14:08
44

If anyone wonders, Drawnonward's solution would look like this in Swift:

func webView(webView: UIWebView!, shouldStartLoadWithRequest request: NSURLRequest!, navigationType: UIWebViewNavigationType) -> Bool {
    if navigationType == UIWebViewNavigationType.LinkClicked {
        UIApplication.sharedApplication().openURL(request.URL)
        return false
    }
    return true
}
DiegoFrings
  • 3,043
  • 3
  • 26
  • 30
  • any idea how to do this ONLY with URLS that have https:// http:// or mailto:? using swift? Question here needs a swift answer! http://stackoverflow.com/questions/2532453/force-a-webview-link-to-launch-safari – Jed Grant Jun 02 '15 at 16:31
  • @JedGrant swift version now on http://stackoverflow.com/questions/2532453/force-a-webview-link-to-launch-safari/30648750#30648750 – Carl Sharman Jun 04 '15 at 16:04
  • 2
    make sure to use `UIWebViewDelegate` – Jay Mayu Jun 26 '15 at 11:23
28

One quick comment to user306253's answer: caution with this, when you try to load something in the UIWebView yourself (i.e. even from the code), this method will prevent it to happened.

What you can do to prevent this (thanks Wade) is:

if (inType == UIWebViewNavigationTypeLinkClicked) {
    [[UIApplication sharedApplication] openURL:[inRequest URL]];
    return NO;
}

return YES;

You might also want to handle the UIWebViewNavigationTypeFormSubmitted and UIWebViewNavigationTypeFormResubmitted types.

MonsieurDart
  • 6,005
  • 2
  • 43
  • 45
16

The other answers have one problem: they rely on the action you do and not on the link itself to decide whether to load it in Safari or in webview.

Now sometimes this is exactly what you want, which is fine; but some other times, especially if you have anchor links in your page, you want really to open only external links in Safari, and not internal ones. In that case you should check the URL.host property of your request.

I use that piece of code to check whether I have a hostname in the URL that is being parsed, or if it is embedded html:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    static NSString *regexp = @"^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])[.])+([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regexp];

    if ([predicate evaluateWithObject:request.URL.host]) {
        [[UIApplication sharedApplication] openURL:request.URL];
        return NO; 
    } else {
        return YES; 
    }
}

You can of course adapt the regular expression to fit your needs.

KPM
  • 10,558
  • 3
  • 45
  • 66
  • Note: the regular expression is derived from http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address – KPM Oct 02 '12 at 22:22
  • 1
    Yes to the point about filtering incoming requests, no to the hostname parsing. A better approach would be to filter based on the URL scheme. On iOS 8.4 (simulator), I got "applewebdata" used as the scheme for anchor links, but that may vary with target version. – MandisaW Feb 29 '16 at 20:34
  • Good idea MandisaW. To allow anchor links I check for the "file" scheme `if (request.URL?.scheme != "file") ` – David Douglas Aug 08 '16 at 21:17
7

In Swift you can use the following code:

extension YourViewController: UIWebViewDelegate {
    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
        if let url = request.url, navigationType == UIWebView.NavigationType.linkClicked {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
            return false
        }
        return true
    }

}

Make sure you check for the URL value and the navigationType.

BaseZen
  • 8,650
  • 3
  • 35
  • 47
Antoine
  • 23,526
  • 11
  • 88
  • 94
3

App rejection Note:

Finally UIWbView is dead and Apple will not longer accept it.

Apple started sending email to all the App owner who are still using UIWebView:

Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs.

Apple takes User Privacy very seriously and it is obvious that they won’t allow insecure webview.

So do remove UIWebView from your app as soon as possible. don't use try to use UIWebView in new created app and I Prefer to using WKWebView if possible

ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See https://developer.apple.com/documentation/uikit/uiwebview for more information.

Example:

import UIKit
import WebKit

class WebInfoController: UIViewController,WKNavigationDelegate {

    var webView : WKWebView = {
        var webview = WKWebView()
        return webview
    }()

    var _fileName : String!

    override func viewDidLoad() {
        self.view.addSubview(webView)
        webView.fillSuperview()
        let url = Bundle.main.url(forResource: _fileName, withExtension: "html")!
        webView.loadFileURL(url, allowingReadAccessTo: url)
        let request = URLRequest(url: url)
        webView.load(request)
    }


    func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) {
        print(error.localizedDescription)
    }
    func webView(webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("Strat to load")
    }
    func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
        print("finish to load")
    }
}
Nazmul Hasan
  • 10,130
  • 7
  • 50
  • 73
1

Here's the Xamarin iOS equivalent of drawnonward's answer.

class WebviewDelegate : UIWebViewDelegate {
    public override bool ShouldStartLoad (UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType) {
        if (navigationType == UIWebViewNavigationType.LinkClicked) {
            UIApplication.SharedApplication.OpenUrl (request.Url);
            return false;
        }
        return true;
    }
}
danfordham
  • 980
  • 9
  • 15
1

The accepted answer does not work.

If your page loads URLs via Javascript, the navigationType will be UIWebViewNavigationTypeOther. Which, unfortunately, also includes background page loads such as analytics.

To detect page navigation, you need to compare the [request URL] to the [request mainDocumentURL].

This solution will work in all cases:

- (BOOL)webView:(UIWebView *)view shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)type
{
    if ([[request URL] isEqual:[request mainDocumentURL]])
    {
        [[UIApplication sharedApplication] openURL:[request URL]];
        return NO;
    }
    else
    {       
        return YES;
    }
}
IanS
  • 1,459
  • 1
  • 18
  • 23
0

In my case I want to make sure that absolutely everything in the web view opens Safari except the initial load and so I use...

- (BOOL)webView:(UIWebView *)inWeb shouldStartLoadWithRequest:(NSURLRequest *)inRequest navigationType:(UIWebViewNavigationType)inType {
     if(inType != UIWebViewNavigationTypeOther) {
        [[UIApplication sharedApplication] openURL:[inRequest URL]];
        return NO;
     }
     return YES;
}
Michael Platt
  • 516
  • 1
  • 5
  • 13
0

UIWebView and UIWebViewDelegate are deprecated. You won't be allowed to push an update to the Appstore with it. Reference

Use WKWebView and WKNavigationDelegate

Sample code:

class YourClass: WKNavigationDelegate {

    override public func viewDidLoad() {
        super.viewDidLoad()
        let webView = WKWebView()
        webView.navigationDelegate = self
        self.view.addaddSubview(webView)
    }
    
    public func webView(_ webView: WKWebView,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let cred = URLCredential(trust: challenge.protectionSpace.serverTrust!)
        completionHandler(.useCredential, cred)
    }

    public func webView(_ webView: WKWebView,
                    decidePolicyFor navigationAction: WKNavigationAction,
                    decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
    
        self.webView.load(navigationAction.request)
        decisionHandler(.allow)
    }
}
Henadzi Rabkin
  • 6,834
  • 3
  • 32
  • 39