Guideline 4.0
Guideline 4.0 - Design: Web Page Opens in Safari Before Returning to App
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
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()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)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.
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).
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:).
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.
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
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.
Before & After
// 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
}
// 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.
// Opens terms in Safari — unnecessary app exit
Button("Terms of Service") {
UIApplication.shared.open(
URL(string: "https://example.com/terms")!
)
}
// 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
- Guideline 4.0 - Design: App Does Not Include iOS Features
- Guideline 4.0 - Design: App Looks Like a Website
- Guideline 4.0 - Design: Apple Pay Button Not Following Guidelines
- Guideline 4.0 - Design: Blurry Icons or Low-Resolution Assets
- Guideline 4.0 - Design: Broken Layout on iPad
- Guideline 4.0 - Design: Content Clipped by Notch or Safe Area