swiftui-webkit
This skill provides patterns and best practices for embedding web content in SwiftUI apps using Apple's native WebKit APIs for iOS 26 and later. Use it when building features like in-app article viewers, help documentation, or HTML-based content that requires navigation control, JavaScript execution, or state observation without resorting to legacy UIKit wrappers or third-party web view libraries.
git clone --depth 1 https://github.com/dpearson2699/swift-ios-skills /tmp/swiftui-webkit && cp -r /tmp/swiftui-webkit/skills/swiftui-webkit ~/.claude/skills/swiftui-webkitSKILL.md
# SwiftUI WebKit
Embed and manage web content in SwiftUI using the native WebKit-for-SwiftUI APIs introduced for iOS 26, iPadOS 26, macOS 26, and visionOS 26. Use this skill when the app needs an integrated web surface, app-owned HTML content, JavaScript-backed page interaction, or custom navigation policy control.
## Contents
- [Choose the Right Web Container](#choose-the-right-web-container)
- [Displaying Web Content](#displaying-web-content)
- [Loading and Observing with WebPage](#loading-and-observing-with-webpage)
- [Navigation Policies](#navigation-policies)
- [JavaScript Integration](#javascript-integration)
- [Local Content and Custom URL Schemes](#local-content-and-custom-url-schemes)
- [WebView Customization](#webview-customization)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Choose the Right Web Container
Use the narrowest tool that matches the job.
| Need | Default choice |
|---|---|
| Embedded app-owned web content in SwiftUI | `WebView` + `WebPage` |
| Simple external site presentation with Safari behavior | `SFSafariViewController` |
| OAuth or third-party sign-in | `ASWebAuthenticationSession` |
| Back-deploy below iOS 26 or use missing legacy-only WebKit features | `WKWebView` fallback |
Prefer `WebView` and `WebPage` for modern SwiftUI apps targeting iOS 26+. Apple’s WWDC25 guidance explicitly recommends migrating SwiftUI apps away from UIKit/AppKit WebKit wrappers when possible.
Do not use embedded web views for OAuth. That stays an `ASWebAuthenticationSession` flow.
## Displaying Web Content
Use the simple `WebView(url:)` form when the app only needs to render a URL and SwiftUI state drives navigation.
```swift
import SwiftUI
import WebKit
struct ArticleView: View {
let url: URL
var body: some View {
WebView(url: url)
}
}
```
Create a `WebPage` when the app needs to load requests directly, observe state, call JavaScript, or customize navigation behavior.
```swift
@Observable
@MainActor
final class ArticleModel {
let page = WebPage()
func load(_ url: URL) async throws {
for try await _ in page.load(URLRequest(url: url)) {
}
}
}
struct ArticleDetailView: View {
@State private var model = ArticleModel()
let url: URL
var body: some View {
WebView(model.page)
.task {
try? await model.load(url)
}
}
}
```
See [references/loading-and-observation.md](references/loading-and-observation.md) for full examples.
## Loading and Observing with WebPage
`WebPage` is an `@MainActor` observable type. Use it when you need page state in SwiftUI.
Common loading entry points:
- `load(URLRequest)`
- `load(URL)`
- `load(html:baseURL:)`
- `load(_:mimeType:characterEncoding:baseURL:)`
Common observable properties:
- `title`
- `url`
- `isLoading`
- `estimatedProgress`
- `currentNavigationEvent`
- `backForwardList`
```swift
struct ReaderView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.navigationTitle(page.title ?? "Loading")
.overlay {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
}
}
.task {
do {
for try await _ in page.load(URLRequest(url: URL(string: "https://example.com")!)) {
}
} catch {
// Handle load failure.
}
}
}
}
```
When you need to react to every navigation, observe the navigation sequence rather than only checking a single property.
```swift
Task {
for await event in page.navigations {
// Handle finish, redirect, or failure events.
}
}
```
See [references/loading-and-observation.md](references/loading-and-observation.md) for stronger patterns and the load-sequence examples.
## Navigation Policies
Use `WebPage.NavigationDeciding` to allow, cancel, or customize navigations based on the request or response.
Typical uses:
- keep app-owned domains inside the embedded web view
- cancel external domains and hand them off with `openURL`
- intercept special callback URLs
- tune `NavigationPreferences`
```swift
@MainActor
final class ArticleNavigationDecider: WebPage.NavigationDeciding {
var urlToOpenExternally: URL?
func decidePolicy(
for action: WebPage.NavigationAction,
preferences: inout WebPage.NavigationPreferences
) async -> WKNavigationActionPolicy {
guard let url = action.request.url else { return .allow }
if url.host == "example.com" {
return .allow
}
urlToOpenExternally = url
return .cancel
}
}
```
Keep app-level deep-link routing in the navigation skill. This skill owns navigation that happens inside embedded web content.
See [references/navigation-and-javascript.md](references/navigation-and-javascript.md) for complete patterns.
## JavaScript Integration
Use `callJavaScript(_:arguments:in:contentWorld:)` to evaluate JavaScript functions against the page.
```swift
let script = """
const headings = [...document.querySelectorAll('h1, h2')];
return headings.map(node => ({
id: node.id,
text: node.textContent?.trim()
}));
"""
let result = try await page.callJavaScript(script)
let headings = result as? [[String: Any]] ?? []
```
You can pass values through the `arguments` dictionary and cast the returned `Any` into the Swift type you actually need.
```swift
let result = try await page.callJavaScript(
"return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;",
arguments: ["sectionID": selectedSectionID]
)
```
Important boundary: the native SwiftUI WebKit API clearly supports Swift-to-JavaScript calls, but it does not expose an obvious direct replacement for `WKScriptMessageHandler`. If you need coarse JS-to-native signaling, a custom navigation oDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, AlarmButton stop and snooze actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
Build iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
Implement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
Optimize App Store product pages for search visibility and conversion. Use for App Store Optimization (ASO), keyword research, app name/subtitle/keyword-field strategy, conversion-focused descriptions and promotional text, screenshot captions and ordering, Custom Product Pages with assigned search keywords, In-App Events, Product Page Optimization tests, localized metadata, ratings/review strategy, and in-app review prompt timing with RequestReviewAction or AppStore.requestReview. Also use when routing ASO vs App Store review, privacy/ATT, or StoreKit implementation boundaries.
Prepare for App Store review and prevent rejections. Covers App Store review guidelines, app rejection reasons, PrivacyInfo.xcprivacy privacy manifest requirements, required API reason codes, in-app purchase IAP and StoreKit rules, App Store Guidelines compliance, ATT App Tracking Transparency, EU DMA Digital Markets Act, HIG compliance checklist, app submission preparation, review preparation, metadata requirements, entitlements, widgets, and Live Activities review rules. Use when preparing for App Store submission, fixing rejection reasons, auditing privacy manifests, implementing ATT consent flow, configuring StoreKit IAP, or checking HIG compliance.