← Files SwiftUI ExpertARCHIVED FILE

skills/swiftui-expert-skill/references/webkit-integration.md

9.5 KB · Oct 5, 2026 · 18:31 UTC

↓ Download file

# WebKit Integration in SwiftUI

> `WebView` and `WebPage` require iOS 26, macOS 26, or visionOS 26 and are **unavailable on watchOS and tvOS**. A few members require the aligned 27 releases; those are called out inline. Both types are `@MainActor`.

## Table of Contents

- [WebView](#webview)
- [WebPage](#webpage)
- [Configuration and Data Stores](#configuration-and-data-stores)
- [Loading Content](#loading-content)
- [Observing Navigation](#observing-navigation)
- [Deciding Navigation Policy](#deciding-navigation-policy)
- [Calling JavaScript](#calling-javascript)
- [Find Navigator](#find-navigator)
- [View Modifiers](#view-modifiers)
- [Exporting PDF and Images](#exporting-pdf-and-images)
- [Custom URL Schemes](#custom-url-schemes)

---

## WebView

`WebView` has two initializers: one for a bare URL, one backed by a `WebPage`.

```swift
import SwiftUI
import WebKit

WebView(url: URL(string: "https://www.swift.org"))
```

`WebView(url:)` is enough for read-only display; the view reloads when the URL changes. Use the `WebPage` initializer when you need the page's title, loading state, back-forward list, JavaScript, or navigation policy.

## WebPage

`WebPage` is an observable `@MainActor` class that owns the web content. Because it is observable, reading its properties in a `body` re-renders automatically.

```swift
struct BrowserView: View {
    @State private var page = WebPage()

    var body: some View {
        NavigationStack {
            WebView(page)
                .navigationTitle(page.title)
        }
        .onAppear {
            _ = page.load(URL(string: "https://www.apple.com"))
        }
    }
}
```

Observable properties include `url`, `title` (a non-optional `String`), `isLoading`, `estimatedProgress`, `backForwardList`, `serverTrust`, and `customUserAgent`. Note that `title` is not optional — don't write `if let title = page.title`.

```swift
ProgressView(value: page.estimatedProgress)
    .opacity(page.isLoading ? 1 : 0)
```

## Configuration and Data Stores

`WebPage.Configuration` is a value type applied at initialization; changing it afterwards has no effect on an existing page.

```swift
var configuration = WebPage.Configuration()
configuration.loadsSubresources = true
configuration.defaultNavigationPreferences.allowsContentJavaScript = true
configuration.websiteDataStore = .nonPersistent()

let page = WebPage(configuration: configuration)
```

`websiteDataStore` is a `WKWebsiteDataStore`. Use `.default()` to share cookies and caches with other pages in the app, or `.nonPersistent()` for private browsing where nothing survives the session. `defaultNavigationPreferences` supplies the baseline `NavigationPreferences` (JavaScript, `ContentMode`) for every navigation; a `NavigationDeciding` can override it per navigation.

Set `page.customUserAgent` on the page itself, not the configuration.

## Loading Content

Every `load` overload returns an `AsyncSequence` of `WebPage.NavigationEvent` values, so you can iterate it to follow that specific navigation — or bind it to `_` when you don't care.

```swift
_ = page.load(URLRequest(url: url))
_ = page.load(html: "<h1>Hello</h1>", baseURL: URL(string: "https://example.com")!)
_ = page.load(data, mimeType: "text/html", characterEncoding: .utf8, baseURL: baseURL)
_ = page.load(simulatedRequest: request, responseHTML: html)
_ = page.reload(fromOrigin: false)
page.stopLoading()
```

`baseURL` resolves relative links and determines the origin for HTML and `Data` loads. Back-forward navigation loads an item from the list:

```swift
if let backItem = page.backForwardList.backItem {
    _ = page.load(backItem)
}
```

## Observing Navigation

`page.navigations` is an `AsyncSequence` of `NavigationEvent` covering all navigations; the per-call sequence returned by `load` covers just that one. Cases are `.startedProvisionalNavigation`, `.receivedServerRedirect`, `.committed`, and `.finished`. Failures surface as thrown `WebPage.NavigationError` values (`.failedProvisionalNavigation`, `.pageClosed`, `.webContentProcessTerminated`, `.invalidURL`).

```swift
.task {
    do {
        for try await event in page.navigations {
            if event == .finished { await indexPage() }
        }
    } catch {
        // handle NavigationError
    }
}
```

For simple loading indicators, prefer the observable `page.isLoading` and `page.estimatedProgress` over consuming the event stream.

## Deciding Navigation Policy

Conform a type to `WebPage.NavigationDeciding` and pass it to the initializer. Every requirement has a default implementation, so implement only what you need.

```swift
struct LinkPolicy: WebPage.NavigationDeciding {
    func decidePolicy(
        for action: WebPage.NavigationAction,
        preferences: inout WebPage.NavigationPreferences
    ) async -> WKNavigationActionPolicy {
        guard action.request.url?.host() != "blocked.example.com" else { return .cancel }
        preferences.allowsContentJavaScript = true
        return .allow
    }

    func decidePolicy(
        for response: WebPage.NavigationResponse
    ) async -> WKNavigationResponsePolicy {
        (response.response as? HTTPURLResponse)?.statusCode == 200 ? .allow : .cancel
    }
}

let page = WebPage(navigationDecider: LinkPolicy())
```

`decideAuthenticationChallengeDisposition(for:)` handles `URLAuthenticationChallenge`, and `willSubmit(formInfo:)` (iOS/macOS/visionOS 27+) observes form submissions. A separate `dialogPresenter:` parameter takes a `WebPage.DialogPresenting` type for JavaScript alerts, confirms, and prompts.

## Calling JavaScript

```swift
let result = try await page.callJavaScript(
    """
    const meta = document.querySelector('meta[name="description"]');
    return meta ? meta.getAttribute('content') : '';
    """
)
let description = result as? String
```

`callJavaScript(_:arguments:in:contentWorld:)` takes a **function body**, so use `return` to produce a value. `arguments` is a `[String: Any]` dictionary whose keys become in-scope variables — pass values that way instead of interpolating strings. `in:` targets a `WebPage.FrameInfo`; `contentWorld:` takes a `WKContentWorld` (`.page`, `.defaultClient`, or a custom world) to isolate your script's globals from the page's own.

## Find Navigator

`WebView` participates in the standard SwiftUI find navigator:

```swift
WebView(page)
    .findNavigator(isPresented: $isSearching)
```

## View Modifiers

Applied to the `WebView`:

| Modifier | Purpose |
|---|---|
| `webViewBackForwardNavigationGestures(_:)` | `.automatic` / `.enabled` / `.disabled` swipe navigation |
| `webViewMagnificationGestures(_:)` | Pinch-to-zoom behavior |
| `webViewLinkPreviews(_:)` | Long-press / force-touch link previews |
| `webViewTextSelection(_:)` | Takes a `TextSelectability`, e.g. `.enabled` |
| `webViewElementFullscreenBehavior(_:)` | Allows HTML element fullscreen |
| `webViewContentBackground(_:)` | Takes a `Visibility` — hide it to show your own background behind the page |
| `webViewContextMenu(menu:)` | macOS only. Builds a menu from a `WebView.ActivatedElementInfo` (its `linkURL`). |
| `webViewScrollPosition(_:)` | Binds a `ScrollPosition` |
| `webViewOnScrollGeometryChange(for:of:action:)` | Observes `ScrollGeometry` changes |
| `webViewScrollInputBehavior(_:for:)` | Enables or disables a `ScrollInputKind` |

On macOS:

```swift
WebView(page)
    .webViewContentBackground(.hidden)
    .background(.regularMaterial)
    .webViewContextMenu { element in
        if let url = element.linkURL {
            ShareLink(item: url)
        }
    }
```

## Exporting PDF and Images

`WebPage` conforms to `Transferable`, so it can be dragged or shared directly. For explicit exports, call `exported(as:)` with a `WebPage.ExportedContentConfiguration`:

```swift
let pdf = try await page.exported(as: .pdf(region: .contents))
let png = try await page.exported(as: .image(region: .rect(bounds), snapshotWidth: 1024))
```

`Region` is either `.contents` or `.rect(_:)`, and both factories accept `allowTransparentBackground`. Both calls return `Data`.

`WebPage` has no web-archive API. Web archives remain a `WKWebView` API (`createWebArchiveData(completionHandler:)`), so reach for `WKWebView` in a representable only when you specifically need `.webarchive` output.

## Custom URL Schemes

`URLSchemeHandler` replies with an `AsyncSequence` of `URLSchemeTaskResult` values — first a `.response`, then one or more `.data` elements. Register handlers in the configuration's `urlSchemeHandlers` dictionary keyed by `URLScheme`.

```swift
struct AssetSchemeHandler: URLSchemeHandler {
    func reply(for request: URLRequest) -> AsyncThrowingStream<URLSchemeTaskResult, any Error> {
        AsyncThrowingStream { continuation in
            guard let url = request.url else {
                continuation.finish(throwing: URLError(.badURL))
                return
            }
            let html = "<h1>\(url.path())</h1>"
            continuation.yield(.response(URLResponse(
                url: url,
                mimeType: "text/html",
                expectedContentLength: -1,
                textEncodingName: "utf-8"
            )))
            continuation.yield(.data(Data(html.utf8)))
            continuation.finish()
        }
    }
}

var configuration = WebPage.Configuration()
if let scheme = URLScheme("myapp") {
    configuration.urlSchemeHandlers[scheme] = AssetSchemeHandler()
}
let page = WebPage(configuration: configuration)
```

`URLScheme(_:)` is failable — the system rejects reserved schemes such as `http` and `https`. Cancellation is expressed by terminating the returned sequence, so honor `Task` cancellation inside it rather than implementing a separate stop callback.

SHA-256: 73a893af77b52c05b3ea8e3e378c82bb9066cb887a2ab7485ec128bb95ba9541