Skip to content
Tolinku
Tolinku
Sign In Start Free
Deep Linking · · 5 min read

SFSafariViewController and Deep Links: Best Practices

By Tolinku Staff
|
Tolinku deep linking fundamentals dashboard screenshot for deep linking blog posts

When you need to show web content inside your iOS app, you have three main options: open Safari directly, use WKWebView, or use SFSafariViewController. Each has different tradeoffs, and each interacts with Universal Links in a distinct way. Choosing the wrong one can create confusing behavior for users who expect deep links to work seamlessly.

This article explains what SFSafariViewController is, how it handles Universal Links, when to use it versus the alternatives, and best practices for integrating it into apps that rely on deep linking.

For WKWebView deep linking, see handling Universal Links in WKWebView. For Universal Links fundamentals, see universal links: everything you need to know.

What Is SFSafariViewController?

SFSafariViewController (introduced in iOS 9) presents a Safari browser interface as a view controller within your app. It is not a custom WebView. It is a sandboxed instance of Safari running inside your app's UI hierarchy.

From the user's perspective it looks like an in-app browser with a Safari-style toolbar. From a technical perspective, it shares Safari's cookie store, content blockers, and JavaScript engine. The key implication for deep linking: it shares Safari's Universal Link behavior.

Apple's reference documentation is at SFSafariViewController.

This is the most important thing to understand. Because SFSafariViewController is technically running Safari (not a custom WebView), Universal Links work from it exactly as they do from the Safari browser. When a user taps a Universal Link inside an SFSafariViewController, iOS will open the target app if it is installed and if the Associated Domains configuration is correct.

This is the opposite of WKWebView, which does not automatically dispatch Universal Links to the system. For a comparison, see handling Universal Links in WKWebView.

Presenting SFSafariViewController

The API is minimal by design:

import SafariServices

class ViewController: UIViewController {

    func openWebContent(url: URL) {
        let safariVC = SFSafariViewController(url: url)
        safariVC.delegate = self
        present(safariVC, animated: true)
    }
}

extension ViewController: SFSafariViewControllerDelegate {

    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        // User tapped Done or the view controller was dismissed
        controller.dismiss(animated: true)
    }
}

Unlike WKWebView, you cannot intercept navigation events or inject JavaScript. SFSafariViewController is intentionally opaque. You know the initial URL you present, and you get a callback when the user is done. Everything in between is Safari's domain.

Customization Options

While SFSafariViewController is opaque in terms of navigation, it does offer some appearance customization:

let safariVC = SFSafariViewController(url: url)

// Tint the control buttons (done button, share button)
safariVC.preferredControlTintColor = .systemBlue

// Set a bar background color
safariVC.preferredBarTintColor = UIColor(named: "BrandColor")

// Choose the dismiss button style
safariVC.dismissButtonStyle = .close // or .cancel, .done

present(safariVC, animated: true)

These options let the in-app browser feel more consistent with your app's visual design without requiring you to build a custom WebView.

The SFSafariViewControllerDelegate protocol provides the safariViewControllerDidFinish callback, which fires when the user explicitly closes the view controller (taps "Done" or "Close"). There is no callback for navigation events within the browser.

This limitation matters for deep link flows. If a Universal Link is tapped inside SFSafariViewController and the target app is installed, iOS will open that app. The SFSafariViewController remains in your app's view hierarchy but loses focus. When the user returns to your app, you may need to handle the state that was active when they left.

A common pattern is to dismiss the SFSafariViewController in your app's scene activation callback:

// In SceneDelegate.swift
func sceneDidBecomeActive(_ scene: UIScene) {
    dismissPresentedSafariViewControllerIfNeeded()
}

func dismissPresentedSafariViewControllerIfNeeded() {
    if let presented = navigationController?.presentedViewController
        as? SFSafariViewController {
        presented.dismiss(animated: false)
    }
}

Authentication: Use ASWebAuthenticationSession Instead

For OAuth and other web-based authentication flows, SFSafariViewController was commonly used in iOS 9-11. Apple deprecated this pattern in iOS 11 and introduced ASWebAuthenticationSession as the dedicated solution.

ASWebAuthenticationSession is specifically designed for authentication redirects. It handles the callback URL and dismisses itself automatically when the redirect is received.

import AuthenticationServices

class AuthViewController: UIViewController {

    var authSession: ASWebAuthenticationSession?

    func startOAuthFlow() {
        let authURL = URL(string:
            "https://auth.provider.com/oauth/authorize?client_id=YOUR_ID&redirect_uri=yourapp://auth/callback"
        )!
        let callbackScheme = "yourapp"

        authSession = ASWebAuthenticationSession(
            url: authURL,
            callbackURLScheme: callbackScheme
        ) { [weak self] callbackURL, error in
            guard error == nil, let callbackURL = callbackURL else {
                return
            }
            self?.handleAuthCallback(callbackURL)
        }

        authSession?.presentationContextProvider = self
        authSession?.prefersEphemeralWebBrowserSession = false
        authSession?.start()
    }
}

extension AuthViewController: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession)
        -> ASPresentationAnchor {
        return view.window!
    }
}

Apple's documentation: ASWebAuthenticationSession.

Key advantages over SFSafariViewController for auth:

  • The callback URL is handled automatically.
  • Ephemeral sessions (prefersEphemeralWebBrowserSession = true) avoid sharing cookies with Safari.
  • The system prompt gives users transparency about what data is shared.

Use SFSafariViewController for general web content browsing. Use ASWebAuthenticationSession for authentication callbacks.

Because SFSafariViewController shares Safari's cookie store, users who are logged into a website in Safari will also be logged in when you open that site via SFSafariViewController. This is often desirable (frictionless experience) but has implications:

  • Single sign-on scenarios. If your app and your website share a session cookie, users logged in to Safari will appear logged in inside SFSafariViewController.
  • Privacy considerations. iOS 11 changed cookie access rules. Third-party cookie access inside SFSafariViewController requires explicit user interaction as per the ITP (Intelligent Tracking Prevention) policy.
  • Inconsistent state. If your web content needs to know the user's app auth state, you will need to pass that state via URL parameters or use WKWebView with a custom token exchange.

When to Choose Each Option

Use Case Recommended Option
Display external web content with cookies SFSafariViewController
OAuth / web-based authentication ASWebAuthenticationSession
Custom web content with JavaScript interaction WKWebView
Full browsing experience outside your app Open in Safari
Controlled in-app browser with navigation hooks WKWebView

SFSafariViewController is the right choice when you want the full Safari experience (cookies, extensions, autofill, content blockers) inside your app with minimal implementation effort. WKWebView is right when you need to control or observe navigation, inject JavaScript, or embed web content tightly into your app's layout.

For how Universal Links behave in Safari, see universal links in Safari: behavior and edge cases.

When Universal Links open from SFSafariViewController, the user transitions from your app's in-app browser directly to another app. This can be disorienting if not expected.

A few UX patterns that help:

Handle the return gracefully. When the user returns to your app after following a Universal Link out, your app should be in a sensible state. If the SFSafariViewController was part of a flow, consider whether it should still be visible.

Do not use SFSafariViewController for your own Universal Links. If you are presenting a URL that is a Universal Link for your own app, you will get a confusing loop: the SFSafariViewController loads the URL, iOS detects the Universal Link, opens your app, which then opens SFSafariViewController again. Handle your own Universal Links natively.

Tolinku and SFSafariViewController

Tolinku manages your AASA files and routes Universal Links to the correct destinations. Because SFSafariViewController is backed by Safari, Tolinku-managed Universal Links resolve correctly when tapped inside it. No special configuration is needed on the SFSafariViewController side.

Where Tolinku adds value is in the routing layer. The deep linking feature lets you configure routes in a dashboard rather than hardcoding them in your app. When a Universal Link resolves inside SFSafariViewController and opens your app, Tolinku's routing rules determine which screen to show. See the Universal Links developer guide for setup details.

For the complete Universal Links guide, see universal links: everything you need to know.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.