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:
```swift
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:
```swift
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