How to Implement Safari View in SwiftUI
SwiftUI has no built-in SafariView, so you wrap SFSafariViewController in a
UIViewControllerRepresentable struct and present it with .sheet.
The user gets a full, sandboxed in-app browser — cookies, Reader Mode, and all — in about 10 lines.
import SwiftUI
import SafariServices
struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
SFSafariViewController(url: url)
}
func updateUIViewController(_ vc: SFSafariViewController, context: Context) {}
}
// In any SwiftUI view:
.sheet(isPresented: $showSafari) {
SafariView(url: URL(string: "https://apple.com")!)
.ignoresSafeArea()
}
Full implementation
The complete version adds an initializer that exposes the two most useful
SFSafariViewController.Configuration options — Reader Mode and bar collapsing —
plus preferred tint colours so the chrome matches your app. Everything is still presented
modally via .sheet, which is the only layout that works correctly with
SFSafariViewController on iPhone.
import SwiftUI
import SafariServices
// MARK: - Reusable SafariView wrapper
struct SafariView: UIViewControllerRepresentable {
let url: URL
private let configuration: SFSafariViewController.Configuration
init(
url: URL,
entersReaderIfAvailable: Bool = false,
barCollapsingEnabled: Bool = true
) {
self.url = url
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = entersReaderIfAvailable
config.barCollapsingEnabled = barCollapsingEnabled
self.configuration = config
}
func makeUIViewController(context: Context) -> SFSafariViewController {
let vc = SFSafariViewController(url: url, configuration: configuration)
vc.preferredBarTintColor = .systemBackground
vc.preferredControlTintColor = .systemBlue
vc.dismissButtonStyle = .done
return vc
}
func updateUIViewController(
_ uiViewController: SFSafariViewController,
context: Context
) {}
}
// MARK: - Example screen
struct BrowserDemoView: View {
@State private var showSafari = false
private let docsURL = URL(string: "https://developer.apple.com/documentation/swiftui")!
var body: some View {
NavigationStack {
VStack(spacing: 24) {
Image(systemName: "safari.fill")
.font(.system(size: 64))
.foregroundStyle(.blue)
Text("SwiftUI Safari View")
.font(.title2.bold())
Text("Opens a full in-app browser with\nReader Mode, history, and sharing.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Button {
showSafari = true
} label: {
Label("Open SwiftUI Docs", systemImage: "safari")
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.accessibilityLabel("Open SwiftUI documentation in Safari")
.padding(.horizontal)
}
.navigationTitle("Browser Demo")
}
.sheet(isPresented: $showSafari) {
SafariView(url: docsURL, entersReaderIfAvailable: false)
.ignoresSafeArea()
}
}
}
#Preview {
BrowserDemoView()
}
How it works
-
UIViewControllerRepresentable bridge.
The
SafariViewstruct adoptsUIViewControllerRepresentable, which is SwiftUI's mechanism for hosting any UIKit view controller inside a SwiftUI hierarchy. You implement two required methods:makeUIViewController(called once) andupdateUIViewController(called on every SwiftUI state change). -
SFSafariViewController.Configuration.
The
Configurationobject is created before the controller and passed to its initializer. SettingentersReaderIfAvailable = trueauto-activates Reader Mode when the page supports it — useful for article-heavy apps.barCollapsingEnabledlets the address bar hide on scroll for more reading space. -
Tint customisation via UIKit properties.
preferredBarTintColorandpreferredControlTintColorare set on the returnedSFSafariViewControllerinstance insidemakeUIViewController. These mirror your app's colour scheme;.systemBackgroundadapts automatically to Dark Mode. -
Modal presentation with .sheet.
SFSafariViewControllermust be presented modally — embedding it inside aNavigationStackorVStackdirectly causes layout and interaction bugs. The.sheet(isPresented:)modifier gives the controller a proper modal context on both iPhone and iPad. -
.ignoresSafeArea() on the sheet content.
Without
.ignoresSafeArea(), the Safari toolbar is inset by the sheet's safe area insets on iPhones with home indicators, producing an awkward gap. Applying it to theSafariViewinside the sheet body fixes this.
Variants
Reader Mode for articles
Pass entersReaderIfAvailable: true when linking to long-form articles.
If the destination page is not Reader-compatible, Safari silently falls back to normal mode.
struct ArticleView: View {
@State private var showArticle = false
let articleURL = URL(string: "https://www.swift.org/blog/")!
var body: some View {
Button("Read Swift Blog") {
showArticle = true
}
.sheet(isPresented: $showArticle) {
SafariView(
url: articleURL,
entersReaderIfAvailable: true // ← auto Reader Mode
)
.ignoresSafeArea()
}
}
}
Full-screen immersive browser
Replace .sheet with .fullScreenCover for a cinematic, edge-to-edge
browser with no sheet drag-to-dismiss handle. The user still gets the built-in Done button
from dismissButtonStyle = .done (set in makeUIViewController) to close
the view. This pattern works well for OAuth flows or in-app onboarding pages where you want the
browser to feel like part of your app rather than a temporary overlay.
Common pitfalls
-
Missing .ignoresSafeArea(). When
SafariViewis the root view of a.sheet, SwiftUI adds safe-area padding around it. Omitting.ignoresSafeArea()results in a visible gap below the Safari toolbar on iPhone 15/16 models. Always apply it directly to theSafariViewcall inside the sheet closure. -
SFSafariViewController cannot be embedded inline. Unlike
WKWebView, Apple explicitly prohibits embeddingSFSafariViewControllerinside a tab bar, navigation stack column, or any non-modal container. Doing so produces broken touch handling and crashes on some OS versions. Always use.sheetor.fullScreenCover. -
No URL interception or JavaScript injection.
SFSafariViewControlleris a privacy sandbox — you cannot read the current URL, inject scripts, or intercept navigation. If your feature requires any of those, you need a fullWKWebView-based implementation instead (see how-to-build-web-view). -
VoiceOver is handled automatically. You do not need to add custom accessibility
modifiers to
SafariViewitself — Safari's native UI is fully accessible. However, the button that opens the Safari sheet should have a descriptive.accessibilityLabelthat names the destination, not just "Open Link".
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement safari view in SwiftUI for iOS 17+. Use SFSafariViewController and UIViewControllerRepresentable (SafariView). Expose entersReaderIfAvailable and preferredControlTintColor as parameters. Present via .sheet with .ignoresSafeArea(). Make it accessible (VoiceOver labels on the trigger button). Add a #Preview with realistic sample data.
In the Soarias Build phase, paste this prompt into a feature ticket to let Claude Code scaffold
the SafariView wrapper and wire it to your existing Link or
Button components in one shot.
Related
FAQ
Does this work on iOS 16?
SFSafariViewController has been available since iOS 9, and
UIViewControllerRepresentable since SwiftUI's first release on iOS 13.
The wrapper shown here compiles and runs correctly on iOS 16. The only iOS 17-specific
feature referenced is the #Preview macro in the preview — swap it for a
PreviewProvider if your deployment target is iOS 16.
Can I intercept URLs or inject JavaScript into SafariView?
SFSafariViewController runs in a separate process
for privacy: it shares the system cookie jar with Safari but exposes no navigation delegate or
script handler to your app. If you need URL interception, OAuth callback handling, or DOM
manipulation, use WKWebView wrapped in a UIViewControllerRepresentable
(see how-to-build-web-view).
For OAuth specifically, prefer ASWebAuthenticationSession instead of
SFSafariViewController — it handles the redirect URL callback natively.
What is the UIKit equivalent?
SFSafariViewController directly and present it
with present(safariVC, animated: true) from any
UIViewController. The SwiftUI wrapper shown on this page does exactly the same
thing under the hood — makeUIViewController returns the
SFSafariViewController and SwiftUI's hosting machinery calls
present for you when the .sheet activates.
Last reviewed: 2026-05-11 by the Soarias team.