{"id":2044,"date":"2026-08-16T09:00:00","date_gmt":"2026-08-16T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=2044"},"modified":"2026-03-07T03:50:29","modified_gmt":"2026-03-07T08:50:29","slug":"sfsafariviewcontroller-deep-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/sfsafariviewcontroller-deep-links\/","title":{"rendered":"SFSafariViewController and Deep Links: Best Practices"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For WKWebView deep linking, see <a href=\"https:\/\/tolinku.com\/blog\/wkwebview-universal-links\/\">handling Universal Links in WKWebView<\/a>. For Universal Links fundamentals, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">universal links: everything you need to know<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Is SFSafariViewController?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;s UI hierarchy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From the user&#39;s perspective it looks like an in-app browser with a Safari-style toolbar. From a technical perspective, it shares Safari&#39;s cookie store, content blockers, and JavaScript engine. The key implication for deep linking: it shares Safari&#39;s Universal Link behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Apple&#39;s reference documentation is at <a href=\"https:\/\/developer.apple.com\/documentation\/safariservices\/sfsafariviewcontroller\" rel=\"nofollow noopener\" target=\"_blank\">SFSafariViewController<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Universal Links Trigger From SFSafariViewController<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the opposite of WKWebView, which does not automatically dispatch Universal Links to the system. For a comparison, see <a href=\"https:\/\/tolinku.com\/blog\/wkwebview-universal-links\/\">handling Universal Links in WKWebView<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Presenting SFSafariViewController<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The API is minimal by design:<\/p>\n\n\n\n<pre><code class=\"language-swift\">import SafariServices\n\nclass ViewController: UIViewController {\n\n    func openWebContent(url: URL) {\n        let safariVC = SFSafariViewController(url: url)\n        safariVC.delegate = self\n        present(safariVC, animated: true)\n    }\n}\n\nextension ViewController: SFSafariViewControllerDelegate {\n\n    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {\n        \/\/ User tapped Done or the view controller was dismissed\n        controller.dismiss(animated: true)\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;s domain.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Customization Options<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">While SFSafariViewController is opaque in terms of navigation, it does offer some appearance customization:<\/p>\n\n\n\n<pre><code class=\"language-swift\">let safariVC = SFSafariViewController(url: url)\n\n\/\/ Tint the control buttons (done button, share button)\nsafariVC.preferredControlTintColor = .systemBlue\n\n\/\/ Set a bar background color\nsafariVC.preferredBarTintColor = UIColor(named: &quot;BrandColor&quot;)\n\n\/\/ Choose the dismiss button style\nsafariVC.dismissButtonStyle = .close \/\/ or .cancel, .done\n\npresent(safariVC, animated: true)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These options let the in-app browser feel more consistent with your app&#39;s visual design without requiring you to build a custom WebView.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Dismiss Callback and Deep Link Flows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>SFSafariViewControllerDelegate<\/code> protocol provides the <code>safariViewControllerDidFinish<\/code> callback, which fires when the user explicitly closes the view controller (taps &quot;Done&quot; or &quot;Close&quot;). There is no callback for navigation events within the browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A common pattern is to dismiss the SFSafariViewController in your app&#39;s scene activation callback:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ In SceneDelegate.swift\nfunc sceneDidBecomeActive(_ scene: UIScene) {\n    dismissPresentedSafariViewControllerIfNeeded()\n}\n\nfunc dismissPresentedSafariViewControllerIfNeeded() {\n    if let presented = navigationController?.presentedViewController\n        as? SFSafariViewController {\n        presented.dismiss(animated: false)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Authentication: Use ASWebAuthenticationSession Instead<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ASWebAuthenticationSession is specifically designed for authentication redirects. It handles the callback URL and dismisses itself automatically when the redirect is received.<\/p>\n\n\n\n<pre><code class=\"language-swift\">import AuthenticationServices\n\nclass AuthViewController: UIViewController {\n\n    var authSession: ASWebAuthenticationSession?\n\n    func startOAuthFlow() {\n        let authURL = URL(string:\n            &quot;https:\/\/auth.provider.com\/oauth\/authorize?client_id=YOUR_ID&amp;redirect_uri=yourapp:\/\/auth\/callback&quot;\n        )!\n        let callbackScheme = &quot;yourapp&quot;\n\n        authSession = ASWebAuthenticationSession(\n            url: authURL,\n            callbackURLScheme: callbackScheme\n        ) { [weak self] callbackURL, error in\n            guard error == nil, let callbackURL = callbackURL else {\n                return\n            }\n            self?.handleAuthCallback(callbackURL)\n        }\n\n        authSession?.presentationContextProvider = self\n        authSession?.prefersEphemeralWebBrowserSession = false\n        authSession?.start()\n    }\n}\n\nextension AuthViewController: ASWebAuthenticationPresentationContextProviding {\n    func presentationAnchor(for session: ASWebAuthenticationSession)\n        -&gt; ASPresentationAnchor {\n        return view.window!\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apple&#39;s documentation: <a href=\"https:\/\/developer.apple.com\/documentation\/authenticationservices\/aswebauthenticationsession\" rel=\"nofollow noopener\" target=\"_blank\">ASWebAuthenticationSession<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Key advantages over SFSafariViewController for auth:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The callback URL is handled automatically.<\/li>\n<li>Ephemeral sessions (<code>prefersEphemeralWebBrowserSession = true<\/code>) avoid sharing cookies with Safari.<\/li>\n<li>The system prompt gives users transparency about what data is shared.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Use SFSafariViewController for general web content browsing. Use ASWebAuthenticationSession for authentication callbacks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cookie Sharing Behavior<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Because SFSafariViewController shares Safari&#39;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:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Single sign-on scenarios.<\/strong> If your app and your website share a session cookie, users logged in to Safari will appear logged in inside SFSafariViewController.<\/li>\n<li><strong>Privacy considerations.<\/strong> iOS 11 changed cookie access rules. Third-party cookie access inside SFSafariViewController requires explicit user interaction as per the <a href=\"https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/\" rel=\"nofollow noopener\" target=\"_blank\">ITP (Intelligent Tracking Prevention)<\/a> policy.<\/li>\n<li><strong>Inconsistent state.<\/strong> If your web content needs to know the user&#39;s app auth state, you will need to pass that state via URL parameters or use WKWebView with a custom token exchange.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">When to Choose Each Option<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Use Case<\/th>\n<th>Recommended Option<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Display external web content with cookies<\/td>\n<td>SFSafariViewController<\/td>\n<\/tr>\n<tr>\n<td>OAuth \/ web-based authentication<\/td>\n<td>ASWebAuthenticationSession<\/td>\n<\/tr>\n<tr>\n<td>Custom web content with JavaScript interaction<\/td>\n<td>WKWebView<\/td>\n<\/tr>\n<tr>\n<td>Full browsing experience outside your app<\/td>\n<td>Open in Safari<\/td>\n<\/tr>\n<tr>\n<td>Controlled in-app browser with navigation hooks<\/td>\n<td>WKWebView<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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&#39;s layout.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For how Universal Links behave in Safari, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-in-safari\/\">universal links in Safari: behavior and edge cases<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Link UX Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When Universal Links open from SFSafariViewController, the user transitions from your app&#39;s in-app browser directly to another app. This can be disorienting if not expected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A few UX patterns that help:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Handle the return gracefully.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not use SFSafariViewController for your own Universal Links.<\/strong> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku and SFSafariViewController<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Where Tolinku adds value is in the routing layer. The <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">deep linking feature<\/a> 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&#39;s routing rules determine which screen to show. See the <a href=\"https:\/\/tolinku.com\/docs\/developer\/universal-links\/\">Universal Links developer guide<\/a> for setup details.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the complete Universal Links guide, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">universal links: everything you need to know<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Use SFSafariViewController effectively with deep links. Learn when to use it, how it interacts with Universal Links, and UX considerations.<\/p>\n","protected":false},"author":2,"featured_media":2043,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"SFSafariViewController and Deep Links: Best Practices for iOS","rank_math_description":"Use SFSafariViewController effectively with deep links. Learn when to use it, how it interacts with Universal Links, and UX considerations.","rank_math_focus_keyword":"SFSafariViewController deep links","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-sfsafariviewcontroller-deep-links.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-sfsafariviewcontroller-deep-links.png","footnotes":""},"categories":[11],"tags":[648,218,20,315,24,652,651,31,22,649],"class_list":["post-2044","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-deep-linking","tag-app-development","tag-authentication","tag-deep-linking","tag-in-app-browser","tag-ios","tag-oauth","tag-sfsafariviewcontroller","tag-swift","tag-universal-links","tag-webkit"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2044","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=2044"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2044\/revisions"}],"predecessor-version":[{"id":2045,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/2044\/revisions\/2045"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/2043"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=2044"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=2044"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=2044"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}