
Webkit Integration
- 75 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Helps with ai & agent building tasks.
About
webkit-integration is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- webkit-integration
- AI & Agent Building
- AI-coding skill
Webkit Integration by the numbers
- 75 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #5,486 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill webkit-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 75 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
WebKit Integration for SwiftUI
Embed and control web content in SwiftUI apps using the native WebView struct and WebPage observable class. Covers loading, navigation, JavaScript execution, and view customization.
When This Skill Activates
- User wants to display web content inside a SwiftUI app
- User needs to load URLs, HTML strings, or data blobs in a web view
- User asks about JavaScript interop from SwiftUI
- User needs navigation control (back, forward, reload) for embedded web content
- User wants to customize web view behavior (gestures, text selection, link previews)
- User needs to capture snapshots or export PDFs from web content
- User asks about intercepting navigation requests or custom URL schemes
- User wants to configure private browsing or custom user agents
Decision Tree
What do you need?
|
+-- Display a URL or HTML content
| +-- Simple, no interaction needed
| | +-- WebView(url:) --> see webview-basics.md
| +-- Need loading state, reload, custom config
| +-- WebPage + WebView(page) --> see webview-basics.md
|
+-- Navigate programmatically (back, forward, intercept)
| +-- Back/forward list, navigation events
| | +-- see navigation.md
| +-- Intercept or cancel navigation requests
| +-- NavigationDeciding protocol --> see navigation.md
|
+-- Execute JavaScript or communicate with web content
| +-- callJavaScript, arguments, content worlds
| +-- see javascript-advanced.md
|
+-- Capture snapshots, export PDF, web archive
| +-- page.snapshot(), page.pdf(), page.webArchiveData()
| +-- see javascript-advanced.md
|
+-- Handle custom URL schemes
+-- URLSchemeHandler protocol --> see javascript-advanced.mdAPI Availability
| API | Minimum OS | Import |
|---|---|---|
WebView | iOS 26 / macOS 26 | SwiftUI + WebKit |
WebPage | iOS 26 / macOS 26 | WebKit |
WebPage.Configuration | iOS 26 / macOS 26 | WebKit |
NavigationDeciding | iOS 26 / macOS 26 | WebKit |
WKContentWorld | iOS 14 / macOS 11 | WebKit |
WKSnapshotConfiguration | iOS 11 / macOS 10.13 | WebKit |
WKPDFConfiguration | iOS 14 / macOS 11 | WebKit |
Quick Start
Simplest Usage
import SwiftUI
import WebKit
struct BrowserView: View {
var body: some View {
WebView(url: URL(string: "https://developer.apple.com")!)
}
}With WebPage for Full Control
import SwiftUI
import WebKit
struct ControlledBrowserView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
page.load(URLRequest(url: URL(string: "https://developer.apple.com")!))
}
}
}Top Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using WebView(url:) when you need navigation control | No access to back/forward, reload, or events | Use WebPage + WebView(page) |
Forgetting import WebKit alongside import SwiftUI | WebView is in SwiftUI but WebPage requires WebKit | Always import both |
Not observing currentNavigationEvent | Missing loading states, errors go unnoticed | Use onChange(of: page.currentNavigationEvent) |
Calling callJavaScript before page finishes loading | Script fails because DOM is not ready | Wait for .finished navigation event |
| Using persistent data store for private browsing | User data is saved to disk | Use .nonPersistent() on WebsiteDataStore |
Not handling nil return from decidePolicyFor(navigationAction:) | Navigation proceeds when it should be cancelled | Return nil to cancel, return NavigationPreferences to allow |
| Passing JavaScript without argument binding | Vulnerable to injection, hard to debug | Use arguments: parameter for named values |
Review Checklist
- [ ] Using
WebPagewhen any control beyond simple display is needed - [ ] Both
SwiftUIandWebKitare imported - [ ] Navigation events observed for loading indicators and error handling
- [ ] JavaScript execution waits for page to finish loading
- [ ] Private browsing uses
.nonPersistent()data store - [ ] Navigation interception returns correct values (preferences to allow, nil to cancel)
- [ ] JavaScript arguments passed via
arguments:parameter, not string interpolation - [ ] Custom URL scheme handler registered on configuration before page loads
- [ ] Find-in-page enabled with
findNavigator(isPresented:)if needed - [ ] Appropriate gesture and interaction modifiers applied (back/forward, magnification, text selection)
- [ ] Content background customized if needed for visual integration
Reference Files
| File | Contents |
|---|---|
webview-basics.md | WebView creation, WebPage setup, configuration, find-in-page, customization modifiers |
navigation.md | Loading content, back/forward list, navigation events, NavigationDeciding protocol |
javascript-advanced.md | JavaScript execution, content worlds, snapshots, PDF export, custom URL schemes |
Cross-References
- For macOS window management around web views, see
macos/architecture-patterns/ - For navigation architecture that hosts a web view, see
ios/navigation-patterns/ - For Liquid Glass design around web content, see
design/liquid-glass/
References
JavaScript and Advanced Features
JavaScript execution, content worlds for isolation, snapshot capture, PDF export, web archives, and custom URL scheme handling.
JavaScript Execution
Use page.callJavaScript to run scripts in loaded web content. The page must have finished loading before scripts can execute reliably.
Basic Execution
// Execute a script and get the result
let title = try await page.callJavaScript("document.title") as? String
// Execute a script with no return value
try await page.callJavaScript("document.body.style.backgroundColor = 'lightblue'")Passing Arguments
Use the arguments: parameter to pass named values safely into the script. This avoids string interpolation and protects against injection:
// ❌ String interpolation -- vulnerable to injection
let script = "document.getElementById('\(elementId)').textContent = '\(newText)'"
try await page.callJavaScript(script)
// ✅ Named arguments -- safe, readable, debuggable
try await page.callJavaScript(
"document.getElementById(elementId).textContent = newText",
arguments: [
"elementId": elementId,
"newText": newText
]
)Arguments are passed as a dictionary of [String: Any] where keys become variable names in the script scope.
Executing in a Specific Frame
Target a specific frame (e.g., an iframe) rather than the main frame:
try await page.callJavaScript(
"document.title",
in: frameInfo // A WKFrameInfo representing the target frame
)Content Worlds for Isolation
Use WKContentWorld to isolate your JavaScript from the page's scripts. This prevents naming collisions and provides security boundaries:
// Execute in the default page world (shares scope with page scripts)
try await page.callJavaScript("window.myVar", contentWorld: .page)
// Execute in the default client world (isolated from page scripts)
try await page.callJavaScript("window.myVar", contentWorld: .defaultClient)
// Execute in a custom named world (fully isolated namespace)
let appWorld = WKContentWorld.world(name: "myApp")
try await page.callJavaScript(
"var myState = { count: 0 }; myState.count",
contentWorld: appWorld
)| Content World | Isolation | Use Case |
|---|---|---|
.page | None -- shares scope with page scripts | Reading page variables, interacting with page libraries |
.defaultClient | Isolated from page scripts | General app-to-web communication |
.world(name:) | Fully isolated namespace | Multiple independent script contexts |
Complete JavaScript Example
import SwiftUI
import WebKit
struct JavaScriptDemoView: View {
@State private var page = WebPage()
@State private var extractedData = ""
var body: some View {
VStack {
Text(extractedData)
.font(.caption)
.padding()
WebView(page)
}
.onChange(of: page.currentNavigationEvent) { _, event in
if case .finished = event {
Task {
await extractPageData()
}
}
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
func extractPageData() async {
do {
// Get page title
let title = try await page.callJavaScript("document.title") as? String ?? "Unknown"
// Count links using named arguments for the tag name
let linkCount = try await page.callJavaScript(
"document.getElementsByTagName(tag).length",
arguments: ["tag": "a"]
) as? Int ?? 0
// Modify page appearance in an isolated world
let appWorld = WKContentWorld.world(name: "myApp")
try await page.callJavaScript(
"""
document.querySelectorAll('a').forEach(function(link) {
link.style.color = color;
});
""",
arguments: ["color": "blue"],
contentWorld: appWorld
)
extractedData = "Title: \(title) | Links: \(linkCount)"
} catch {
extractedData = "JavaScript error: \(error.localizedDescription)"
}
}
}Snapshots
Capture the current web view content as an image.
import WebKit
// Basic snapshot
let image = try await page.snapshot(WKSnapshotConfiguration())
// Configured snapshot (specific rect, after screen updates)
var snapshotConfig = WKSnapshotConfiguration()
snapshotConfig.rect = CGRect(x: 0, y: 0, width: 400, height: 300)
snapshotConfig.afterScreenUpdates = true
let croppedImage = try await page.snapshot(snapshotConfig)Snapshot in SwiftUI
struct SnapshotView: View {
@State private var page = WebPage()
@State private var capturedImage: Image?
var body: some View {
VStack {
if let capturedImage {
capturedImage
.resizable()
.scaledToFit()
.frame(height: 200)
}
WebView(page)
Button("Capture Snapshot") {
Task {
let snapshot = try await page.snapshot(WKSnapshotConfiguration())
capturedImage = snapshot
}
}
.buttonStyle(.bordered)
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}PDF Export
Generate a PDF from the current web content.
import WebKit
// Basic PDF
let pdfData = try await page.pdf(configuration: WKPDFConfiguration())
// Configured PDF (specific print rect)
var pdfConfig = WKPDFConfiguration()
pdfConfig.rect = CGRect(x: 0, y: 0, width: 612, height: 792) // US Letter size in points
let letterPDF = try await page.pdf(configuration: pdfConfig)Save PDF to File
struct PDFExportView: View {
@State private var page = WebPage()
@State private var showShareSheet = false
@State private var pdfURL: URL?
var body: some View {
VStack {
WebView(page)
Button("Export PDF") {
Task {
let pdfData = try await page.pdf(configuration: WKPDFConfiguration())
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent("export.pdf")
try pdfData.write(to: tempURL)
pdfURL = tempURL
showShareSheet = true
}
}
.buttonStyle(.bordered)
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Web Archives
Save the complete web page (HTML, CSS, images) as a web archive for offline viewing.
// Capture web archive data
let archiveData = try await page.webArchiveData()
// Save to file
let archiveURL = FileManager.default.temporaryDirectory
.appendingPathComponent("page.webarchive")
try archiveData.write(to: archiveURL)
// Load a web archive later
let savedData = try Data(contentsOf: archiveURL)
page.load(
data: savedData,
mimeType: "application/x-webarchive",
characterEncoding: "utf-8",
baseURL: nil
)Custom URL Scheme Handling
Intercept requests to custom URL schemes (e.g., myapp://) to serve local content or handle app-specific protocols.
Define a URL Scheme Handler
import WebKit
class AppSchemeHandler: URLSchemeHandler {
func webView(
_ webView: WKWebView,
start urlSchemeTask: any WKURLSchemeTask
) {
guard let url = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(URLError(.badURL))
return
}
// Serve local content based on the URL path
let path = url.path
let responseHTML = """
<html>
<body>
<h1>Local Content</h1>
<p>Served for path: \(path)</p>
</body>
</html>
"""
let data = Data(responseHTML.utf8)
let response = URLResponse(
url: url,
mimeType: "text/html",
expectedContentLength: data.count,
textEncodingName: "utf-8"
)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
func webView(
_ webView: WKWebView,
stop urlSchemeTask: any WKURLSchemeTask
) {
// Handle cancellation if needed
}
}Register the Handler
Register the scheme handler on the configuration before creating the WebPage:
var configuration = WebPage.Configuration()
configuration.setURLSchemeHandler(
AppSchemeHandler(),
forURLScheme: "myapp"
)
let page = WebPage(configuration: configuration)
// Now URLs like "myapp://content/page1" will be handled by AppSchemeHandler
page.load(URLRequest(url: URL(string: "myapp://content/page1")!))Serving Bundle Resources
A common pattern is serving local files from the app bundle via a custom scheme:
class BundleSchemeHandler: URLSchemeHandler {
func webView(
_ webView: WKWebView,
start urlSchemeTask: any WKURLSchemeTask
) {
guard let url = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(URLError(.badURL))
return
}
// Map URL path to bundle resource
let resourceName = url.lastPathComponent
let resourceExtension = url.pathExtension
guard let fileURL = Bundle.main.url(
forResource: resourceName.replacingOccurrences(
of: ".\(resourceExtension)", with: ""
),
withExtension: resourceExtension
),
let data = try? Data(contentsOf: fileURL) else {
urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist))
return
}
let mimeType: String
switch resourceExtension {
case "html": mimeType = "text/html"
case "css": mimeType = "text/css"
case "js": mimeType = "application/javascript"
case "png": mimeType = "image/png"
case "jpg", "jpeg": mimeType = "image/jpeg"
case "svg": mimeType = "image/svg+xml"
default: mimeType = "application/octet-stream"
}
let response = URLResponse(
url: url,
mimeType: mimeType,
expectedContentLength: data.count,
textEncodingName: "utf-8"
)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
func webView(
_ webView: WKWebView,
stop urlSchemeTask: any WKURLSchemeTask
) {
// No-op for synchronous responses
}
}Mistakes to Avoid
String Interpolation in JavaScript
// ❌ Injection risk and hard to debug
let userInput = "'; document.cookie; '"
try await page.callJavaScript("alert('\(userInput)')")
// ✅ Named arguments are escaped and type-safe
try await page.callJavaScript(
"alert(message)",
arguments: ["message": userInput]
)Running JavaScript Before Page Loads
// ❌ Page not ready -- script will fail or return unexpected results
page.load(URLRequest(url: someURL))
let title = try await page.callJavaScript("document.title")
// ✅ Wait for .finished navigation event before executing scripts
// (See navigation.md for the onChange pattern)Registering URL Scheme Handler After Creating WebPage
// ❌ Handler must be set on configuration BEFORE creating the page
let page = WebPage()
// Too late to register a scheme handler
// ✅ Register on configuration first, then create page
var configuration = WebPage.Configuration()
configuration.setURLSchemeHandler(handler, forURLScheme: "myapp")
let page = WebPage(configuration: configuration)Using .page Content World for Sensitive Scripts
// ❌ Page scripts can see and interfere with your variables
try await page.callJavaScript(
"var apiKey = key",
arguments: ["key": secretKey],
contentWorld: .page // Exposed to page scripts
)
// ✅ Use an isolated world for sensitive operations
let secureWorld = WKContentWorld.world(name: "secure")
try await page.callJavaScript(
"var apiKey = key",
arguments: ["key": secretKey],
contentWorld: secureWorld // Isolated from page scripts
)Checklist
- [ ] JavaScript arguments passed via
arguments:parameter, not string interpolation - [ ] Scripts execute only after page navigation reaches
.finished - [ ] Sensitive scripts use isolated
WKContentWorld, not.page - [ ] Snapshot configuration specifies rect if only partial capture is needed
- [ ] PDF configuration sets appropriate page size for the target use case
- [ ] Custom URL scheme handler registered on configuration before
WebPageis created - [ ] URL scheme handler calls
didFailWithErrorfor invalid requests - [ ] URL scheme handler calls
didReceive(response),didReceive(data),didFinishin order - [ ] Error handling wraps all
callJavaScriptcalls in do/catch or try?
Navigation
Loading content, managing back/forward history, observing navigation events, and intercepting navigation with the NavigationDeciding protocol.
Loading Content
WebPage supports three loading methods depending on the content source.
Load a URL
let page = WebPage()
// Load from URL request
page.load(URLRequest(url: URL(string: "https://example.com")!))Load HTML String
let page = WebPage()
page.load(
html: "<h1>Hello</h1><p>Rendered from a string.</p>",
baseURL: URL(string: "https://example.com") // Resolves relative URLs
)Load Raw Data
let page = WebPage()
let pdfData: Data = ... // e.g., fetched from network or bundled file
page.load(
data: pdfData,
mimeType: "application/pdf",
characterEncoding: "utf-8",
baseURL: URL(string: "https://example.com")
)Reload and Stop
// Reload current page
page.reload()
// Reload bypassing cache (fetch fresh from origin)
page.reload(fromOrigin: true)
// Cancel an in-progress load
page.stopLoading()Back/Forward Navigation
WebPage maintains a back/forward list automatically as the user navigates.
Reading the History
// Items the user can go back to
let backItems = page.backForwardList.backList
// Items the user can go forward to (after going back)
let forwardItems = page.backForwardList.forwardList
// Current item
let currentItem = page.backForwardList.currentItemNavigating the History
// Go back to a specific item
if let backItem = page.backForwardList.backList.last {
page.load(backItem)
}Browser Toolbar Example
struct BrowserToolbar: View {
let page: WebPage
var body: some View {
HStack(spacing: 16) {
Button {
if let backItem = page.backForwardList.backList.last {
page.load(backItem)
}
} label: {
Image(systemName: "chevron.left")
}
.disabled(page.backForwardList.backList.isEmpty)
Button {
if let forwardItem = page.backForwardList.forwardList.first {
page.load(forwardItem)
}
} label: {
Image(systemName: "chevron.right")
}
.disabled(page.backForwardList.forwardList.isEmpty)
Button {
page.reload()
} label: {
Image(systemName: "arrow.clockwise")
}
Spacer()
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}Navigation Events
Observe page.currentNavigationEvent to track loading state and detect errors.
Event States
| State | Meaning |
|---|---|
.started | Navigation has begun; show loading indicator |
.finished | Content loaded successfully; hide loading indicator |
.failed | Navigation failed (network error, invalid URL, etc.) |
Observing Events
struct WebBrowserView: View {
@State private var page = WebPage()
@State private var isLoading = false
@State private var loadError: Error?
var body: some View {
VStack {
if isLoading {
ProgressView()
}
if let error = loadError {
ContentUnavailableView(
"Failed to Load",
systemImage: "wifi.exclamationmark",
description: Text(error.localizedDescription)
)
}
WebView(page)
}
.onChange(of: page.currentNavigationEvent) { _, event in
switch event {
case .started:
isLoading = true
loadError = nil
case .finished:
isLoading = false
case .failed:
isLoading = false
// Handle the failure in your UI
default:
break
}
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Waiting for Load Completion
When you need to perform an action (like JavaScript execution) after the page finishes loading, observe the navigation event:
struct ScriptAfterLoadView: View {
@State private var page = WebPage()
@State private var pageTitle = ""
var body: some View {
VStack {
Text(pageTitle)
.font(.headline)
WebView(page)
}
.onChange(of: page.currentNavigationEvent) { _, event in
if case .finished = event {
Task {
if let title = try? await page.callJavaScript("document.title") as? String {
pageTitle = title
}
}
}
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}NavigationDeciding Protocol
Implement NavigationDeciding to intercept and control navigation decisions. This lets you allow, modify, or cancel navigations before they happen.
Protocol Methods
The protocol defines two decision points:
1. `decidePolicyFor(navigationAction:)` -- Called before a request is sent. Return NavigationPreferences to allow, or nil to cancel. 2. `decidePolicyFor(navigationResponse:)` -- Called after a response is received but before content is displayed. Return true to allow, false to cancel.
Basic Implementation
import WebKit
struct MyNavigationDelegate: NavigationDeciding {
/// Decide whether to allow a navigation action (before request is sent)
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
// Allow all navigations with default preferences
return NavigationPreferences()
}
/// Decide whether to display the response (after response is received)
func decidePolicyFor(navigationResponse: NavigationResponse) -> Bool {
// Allow all responses
return true
}
}Filtering by Domain
Block navigation to external domains, keeping the user within allowed sites:
struct DomainFilterDelegate: NavigationDeciding {
let allowedDomains: Set<String>
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
guard let host = navigationAction.request.url?.host else {
return nil // Cancel if no host
}
if allowedDomains.contains(host) {
return NavigationPreferences() // Allow
}
return nil // Cancel -- domain not allowed
}
func decidePolicyFor(navigationResponse: NavigationResponse) -> Bool {
return true
}
}Opening External Links in System Browser
Intercept links that should open outside the app:
import SwiftUI
import WebKit
struct ExternalLinkDelegate: NavigationDeciding {
let internalHost: String
@Environment(\.openURL) private var openURL
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
guard let url = navigationAction.request.url,
let host = url.host else {
return NavigationPreferences()
}
if host == internalHost {
return NavigationPreferences() // Allow internal navigation
}
// Open external links in the system browser
openURL(url)
return nil // Cancel in-app navigation
}
func decidePolicyFor(navigationResponse: NavigationResponse) -> Bool {
return true
}
}Controlling JavaScript per Navigation
struct JavaScriptControlDelegate: NavigationDeciding {
let trustedDomains: Set<String>
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
var preferences = NavigationPreferences()
if let host = navigationAction.request.url?.host,
trustedDomains.contains(host) {
preferences.allowsContentJavaScript = true
} else {
preferences.allowsContentJavaScript = false
}
return preferences
}
func decidePolicyFor(navigationResponse: NavigationResponse) -> Bool {
return true
}
}Complete Browser Example
A full browser view with address bar, back/forward, reload, and loading state:
import SwiftUI
import WebKit
struct MiniBrowser: View {
@State private var page = WebPage()
@State private var urlText = "https://developer.apple.com"
@State private var isLoading = false
@State private var isFindPresented = false
var body: some View {
VStack(spacing: 0) {
// Address bar
HStack {
Button {
if let item = page.backForwardList.backList.last {
page.load(item)
}
} label: {
Image(systemName: "chevron.left")
}
.disabled(page.backForwardList.backList.isEmpty)
Button {
if let item = page.backForwardList.forwardList.first {
page.load(item)
}
} label: {
Image(systemName: "chevron.right")
}
.disabled(page.backForwardList.forwardList.isEmpty)
TextField("URL", text: $urlText)
.textFieldStyle(.roundedBorder)
.onSubmit {
loadURL()
}
if isLoading {
Button {
page.stopLoading()
} label: {
Image(systemName: "xmark")
}
} else {
Button {
page.reload()
} label: {
Image(systemName: "arrow.clockwise")
}
}
}
.padding()
.buttonStyle(.bordered)
.controlSize(.small)
// Web content
WebView(page)
.webViewBackForwardNavigationGestures(.enabled)
.webViewTextSelection(.enabled)
.findNavigator(isPresented: $isFindPresented)
}
.onChange(of: page.currentNavigationEvent) { _, event in
switch event {
case .started:
isLoading = true
case .finished, .failed:
isLoading = false
default:
break
}
}
.onAppear {
loadURL()
}
}
private func loadURL() {
guard let url = URL(string: urlText) else { return }
page.load(URLRequest(url: url))
}
}Mistakes to Avoid
Not Observing Navigation Events
// ❌ No feedback to user about loading state or errors
struct SilentWebView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}
// ✅ Observe events for loading state and error handling
struct ObservantWebView: View {
@State private var page = WebPage()
@State private var isLoading = false
var body: some View {
ZStack {
WebView(page)
if isLoading { ProgressView() }
}
.onChange(of: page.currentNavigationEvent) { _, event in
isLoading = (event == .started)
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Returning Wrong Values from NavigationDeciding
// ❌ Returning NavigationPreferences() when you want to cancel
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
if shouldBlock(navigationAction) {
return NavigationPreferences() // Wrong -- this ALLOWS the navigation
}
return nil
}
// ✅ Return nil to cancel, NavigationPreferences to allow
func decidePolicyFor(navigationAction: NavigationAction) -> NavigationPreferences? {
if shouldBlock(navigationAction) {
return nil // Correct -- cancels the navigation
}
return NavigationPreferences() // Allow
}Checklist
- [ ] Loading state tracked via
currentNavigationEvent - [ ] Error state handled for
.failednavigation events - [ ] Back/forward buttons disabled when history list is empty
- [ ]
NavigationDecidingused if navigation filtering is needed - [ ]
decidePolicyFor(navigationAction:)returnsnilto cancel,NavigationPreferencesto allow - [ ]
decidePolicyFor(navigationResponse:)returnsfalseto cancel,trueto allow - [ ]
reload(fromOrigin: true)used when cache bypass is needed - [ ] External links handled appropriately (in-app vs system browser)
WebView Basics
Creating web views, configuring WebPage, enabling find-in-page, and applying customization modifiers.
WebView Creation
URL-Based (Simplest)
For display-only scenarios where no programmatic control is needed:
import SwiftUI
import WebKit
struct SimpleWebView: View {
var body: some View {
WebView(url: URL(string: "https://example.com")!)
}
}WebPage-Based (Full Control)
For any scenario requiring loading control, navigation, JavaScript, or event observation:
import SwiftUI
import WebKit
struct ControlledWebView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}When to Use Which
| Scenario | Approach |
|---|---|
| Static content display (about page, terms) | WebView(url:) |
| Need reload, back/forward | WebPage + WebView(page) |
| Need loading indicators | WebPage + WebView(page) |
| Need JavaScript execution | WebPage + WebView(page) |
| Need navigation interception | WebPage + WebView(page) |
WebPage Configuration
Configure a WebPage with a WebPage.Configuration for advanced scenarios.
Basic Configuration
var configuration = WebPage.Configuration()
// Allow JavaScript in loaded content
configuration.defaultNavigationPreferences.allowsContentJavaScript = true
// Load subresources (images, CSS, scripts)
configuration.loadsSubresources = true
let page = WebPage(configuration: configuration)Private Browsing
Use a non-persistent data store so no cookies, cache, or history are saved to disk:
var configuration = WebPage.Configuration()
configuration.websiteDataStore = .nonPersistent()
let page = WebPage(configuration: configuration)Custom User Agent
Override the user agent string sent with requests:
let page = WebPage()
page.customUserAgent = "MyApp/1.0 (iOS)"Full Configuration Example
struct ConfiguredBrowserView: View {
@State private var page: WebPage
init() {
var configuration = WebPage.Configuration()
configuration.defaultNavigationPreferences.allowsContentJavaScript = true
configuration.loadsSubresources = true
configuration.websiteDataStore = .nonPersistent()
_page = State(initialValue: WebPage(configuration: configuration))
}
var body: some View {
WebView(page)
.onAppear {
page.customUserAgent = "MyApp/1.0"
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Find in Page
Enable the built-in text search UI using findNavigator:
struct SearchableWebView: View {
@State private var page = WebPage()
@State private var isFindPresented = false
var body: some View {
WebView(page)
.findNavigator(isPresented: $isFindPresented)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Find", systemImage: "magnifyingglass") {
isFindPresented.toggle()
}
}
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Customization Modifiers
Apply these modifiers to WebView to control interaction and appearance.
Gestures
WebView(page)
// Enable swipe left/right for back/forward navigation
.webViewBackForwardNavigationGestures(.enabled)
// Enable pinch-to-zoom
.webViewMagnificationGestures(.enabled)Text and Links
WebView(page)
// Enable text selection in web content
.webViewTextSelection(.enabled)
// Enable link preview on long press / force touch
.webViewLinkPreviews(.enabled)Content Background
Override the web view background color to match your app's design:
WebView(page)
.webViewContentBackground(.color(.systemBackground))Fullscreen Video
Allow elements (like video) to enter fullscreen mode:
WebView(page)
.webViewElementFullscreenBehavior(.enabled)Context Menu
Customize the right-click / long-press context menu:
WebView(page)
.webViewContextMenu { defaultActions in
// Return modified actions or completely custom menu
defaultActions
}Combined Example
struct FullFeaturedWebView: View {
@State private var page = WebPage()
@State private var isFindPresented = false
var body: some View {
WebView(page)
.webViewBackForwardNavigationGestures(.enabled)
.webViewMagnificationGestures(.enabled)
.webViewTextSelection(.enabled)
.webViewLinkPreviews(.enabled)
.webViewContentBackground(.color(.systemBackground))
.webViewElementFullscreenBehavior(.enabled)
.findNavigator(isPresented: $isFindPresented)
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Common Patterns
WebView with Loading Indicator
struct WebViewWithLoading: View {
@State private var page = WebPage()
@State private var isLoading = true
var body: some View {
ZStack {
WebView(page)
if isLoading {
ProgressView()
}
}
.onChange(of: page.currentNavigationEvent) { _, event in
switch event {
case .started:
isLoading = true
case .finished, .failed:
isLoading = false
default:
break
}
}
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Inline HTML Content
struct HTMLContentView: View {
@State private var page = WebPage()
let htmlContent = """
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system; padding: 16px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Hello from SwiftUI</h1>
<p>This is inline HTML content.</p>
</body>
</html>
"""
var body: some View {
WebView(page)
.onAppear {
page.load(
html: htmlContent,
baseURL: nil
)
}
}
}Mistakes to Avoid
Using WebView(url:) When Control Is Needed
// ❌ No way to reload, go back, or detect loading state
struct LimitedView: View {
var body: some View {
WebView(url: URL(string: "https://example.com")!)
}
}
// ✅ Full control through WebPage
struct ControlledView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.onAppear {
page.load(URLRequest(url: URL(string: "https://example.com")!))
}
}
}Forgetting to Import WebKit
// ❌ WebView is in SwiftUI, but WebPage and configuration types require WebKit
import SwiftUI
// Missing: import WebKit
// ✅ Always import both
import SwiftUI
import WebKitNot Waiting for Load Before Interacting
// ❌ JavaScript may fail if page hasn't loaded
page.load(URLRequest(url: someURL))
try await page.callJavaScript("document.title") // Page not ready
// ✅ Wait for navigation event to reach .finished
page.load(URLRequest(url: someURL))
// Observe currentNavigationEvent and call JavaScript after .finishedChecklist
- [ ] Chosen correct WebView initializer (
url:vspage) - [ ] Imported both
SwiftUIandWebKit - [ ] Configured
WebPage.Configurationbefore first load if needed - [ ] Set
customUserAgentif the server requires it - [ ] Used
.nonPersistent()data store for private browsing - [ ] Applied appropriate customization modifiers
- [ ] Added
findNavigatorif text search is needed - [ ] Loading indicator tied to
currentNavigationEvent