Guideline 4.0

Guideline 4.0 - Design: Web Page Opens in Safari Before Returning to App

Low SeverityEasy FixTypical Fix: 1-2 hours0 Reports
Also known as:OAuth login flow opens Safari and redirects back via URL schemePayment or checkout page opens in Safari before returning to appTerms of service or privacy policy opens in Safari instead of in-appEmail verification link opens Safari, then deep links back to the appApp opens Safari for a form or survey, then returns via universal link

Our Take

Apple rejected your app because it opens a web page in Mobile Safari as part of a normal in-app flow, then bounces the user back to the app. This typically happens during OAuth login flows, payment processing, or terms-of-service viewing where the app calls UIApplication.shared.open(url) instead of presenting the web content within the app. The Safari round-trip is disorienting and creates a poor user experience. Apple expects web content to be displayed within the app using SFSafariViewController, ASWebAuthenticationSession, or WKWebView.

Resolution Guide

01

Use ASWebAuthenticationSession for OAuth

This is the recommended way to handle web-based login flows. It presents a system-managed web view and handles the redirect automatically:

   import AuthenticationServices

   let session = ASWebAuthenticationSession(
       url: authURL,
       callbackURLScheme: "myapp"
   ) { callbackURL, error in
       // Handle the OAuth callback
   }
   session.presentationContextProvider = self
   session.prefersEphemeralWebBrowserSession = true
   session.start()
02

Use SFSafariViewController for general web content

For terms of service, privacy policy, help pages, and other web content that doesn't need deep app integration:

   import SafariServices

   let safari = SFSafariViewController(url: termsURL)
   present(safari, animated: true)
03

Use WKWebView for deeply integrated web content

If you need to inject JavaScript, intercept navigation, or customize the web experience, embed a WKWebView directly in your view hierarchy.

04

Remove all UIApplication.shared.open() calls for in-app flows

Search your codebase for UIApplication.shared.open and openURL. Replace any that are part of the normal app flow (not external links the user explicitly requests).

05

Handle OAuth redirects in-app

If using ASWebAuthenticationSession, the callback is handled automatically. If using a custom approach, register your URL scheme in Info.plist and handle it in application(_:open:options:).

06

Test the full authentication flow

Sign out, then sign in again and verify the entire flow stays within the app. The user should never see Safari's UI during normal app usage.

07

Keep "Open in Safari" for external links

It's fine to open external links (e.g., "Visit our website") in Safari with a share button or explicit "Open in Safari" option. The rejection is about automatic bouncing, not user-initiated navigation.

Example Rejection Email

From:Apple App Review Team
Subject:Guideline 4.0 - Design: Web Page Opens in Safari Before
Upon launching the app, a web page in mobile Safari opens first before returning the user to the app. Specifically, when the user taps 'Sign In,' the app opens a login page in Safari, and after authentication the user is redirected back to the app via a URL scheme. This creates a disruptive experience. In-app web content should be displayed using SFSafariViewController or WKWebView to keep users within your app. Next Steps: Please revise your app to present web content within the app and resubmit.

Consider Appealing

Do not appeal. This is a clear UX issue with a direct fix. Replace the Safari redirect with ASWebAuthenticationSession (for auth) or SFSafariViewController (for content) and resubmit. The fix takes less time than waiting for an appeal response.

Generate Appeal

Before & After

Before — Rejected

// Opens Safari for login — user bounces out and back

func signIn() {

let authURL = URL(string: "https://auth.example.com/login?redirect=myapp://callback")!

UIApplication.shared.open(authURL) // opens Safari!

}


// In AppDelegate:

func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {

if url.scheme == "myapp" { handleAuthCallback(url) }

return true

}

After — Approved

// Stays in-app using ASWebAuthenticationSession

import AuthenticationServices


func signIn() {

let authURL = URL(string: "https://auth.example.com/login")!

let session = ASWebAuthenticationSession(

url: authURL,

callbackURLScheme: "myapp"

) { callbackURL, error in

guard let url = callbackURL else { return }

self.handleAuthCallback(url)

}

session.presentationContextProvider = self

session.prefersEphemeralWebBrowserSession = true

session.start()

}

What changed: ASWebAuthenticationSession presents a secure in-app browser sheet for authentication. The user never leaves the app, and the callback is handled seamlessly. Setting prefersEphemeralWebBrowserSession to true avoids sharing Safari cookies.

Before — Rejected

// Opens terms in Safari — unnecessary app exit

Button("Terms of Service") {

UIApplication.shared.open(

URL(string: "https://example.com/terms")!

)

}

After — Approved

// Opens terms in-app with SFSafariViewController

@State private var showTerms = false


Button("Terms of Service") { showTerms = true }

.sheet(isPresented: $showTerms) {

SafariView(url: URL(string: "https://example.com/terms")!)

}


// Helper wrapper for SwiftUI

struct SafariView: UIViewControllerRepresentable {

let url: URL

func makeUIViewController(context: Context) -> SFSafariViewController {

SFSafariViewController(url: url)

}

func updateUIViewController(_ vc: SFSafariViewController, context: Context) {}

}

What changed: SFSafariViewController displays web content in a modal sheet within the app. The user can read the terms and dismiss the sheet to return to exactly where they were, with no Safari bounce.

Community Solutions · 0

Sign in to share your solution.

More Guideline 4 (Design) rejections

View all Guideline 4 rejections