
Swiftui Webkit
- 2.2k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
swiftui-webkit is an agent skill that Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, Jav.
About
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 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 Use the narrowest tool that matches the job Need Default choice Embedded app owned web content in SwiftUI WebView WebPage iOS iPadOS modal browsing with Safari behavior SFSafariViewController macOS or visionOS browse out behavior openURL default browser OAuth or third party sign in ASWebAuthenticationSession Back deploy below iOS 26 or use missing legacy only WebKit features WKWebView fallback Prefer WebView and
- description: "Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation
- Embed and manage web content in SwiftUI using the native WebKit-for-SwiftUI APIs introduced for iOS 26, iPadOS 26, macOS
- - [Choose the Right Web Container](#choose-the-right-web-container)
- Follow swiftui-webkit SKILL.md steps and documented constraints.
- Follow swiftui-webkit SKILL.md steps and documented constraints.
Swiftui Webkit by the numbers
- 2,194 all-time installs (skills.sh)
- +114 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #462 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
swiftui-webkit capabilities & compatibility
- Capabilities
- description: "embeds and controls web content in · embed and manage web content in swiftui using th · [choose the right web container](#choose the r · follow swiftui webkit skill.md steps and documen
- Use cases
- orchestration
What swiftui-webkit says it does
description: "Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, JavaScript execution, observable page state, link interception, local
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 surfac
- [Choose the Right Web Container](#choose-the-right-web-container)
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-webkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.2k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
When should an agent use swiftui-webkit and what problem does it solve?
Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, JavaScript execution, observable page state, link interception, local HTML or data
Who is it for?
Developers invoking swiftui-webkit as documented in the skill source.
Skip if: Skip when requirements fall outside swiftui-webkit documented scope.
When should I use this skill?
Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, JavaScript execution, observable page state, link interception, local HTML or data
What you get
Outputs aligned with the swiftui-webkit SKILL.md workflow and stated deliverables.
- swiftui webview component
- navigation observer hooks
Files
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
- Displaying Web Content
- Loading and Observing with WebPage
- Navigation Policies
- JavaScript Integration
- Local Content and Custom URL Schemes
- WebView Customization
- Common Mistakes
- Review Checklist
- 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 |
| iOS/iPadOS modal browsing with Safari behavior | SFSafariViewController |
| macOS or visionOS browse-out behavior | openURL / default browser |
| 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+ when the new API surface covers the feature. Apple’s WWDC25 guidance frames existing UIKit/AppKit WebKit wrappers in SwiftUI apps as good candidates to try migrating, not as a blanket mandate to delete every fallback.
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.
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.
A WebPage can be associated with only one WebView at a time. Create separate WebPage instances for multiple visible web views.
@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 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:
titleurlisLoadingestimatedProgresscurrentNavigationEventbackForwardList
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.
Task {
do {
for try await event in page.navigations {
// Handle started, redirect, committed, or finished events.
}
} catch {
// Handle WebPage.NavigationError or cancellation.
}
}See 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
@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 for complete patterns.
JavaScript Integration
Use callJavaScript(_:arguments:in:contentWorld:) to evaluate JavaScript functions against the page.
Pass a JavaScript function body, not a wrapped function declaration or call expression. Prefer arguments for Swift-provided values instead of interpolating untrusted strings into the script.
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.
let result = try await page.callJavaScript(
"return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;",
arguments: ["sectionID": selectedSectionID]
)Handle empty and JavaScript null results deliberately: no explicit return produces nil, while an explicit JavaScript null returns NSNull.
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 or callback-URL pattern can work, but document it as a workaround pattern, not a guaranteed one-to-one replacement.
See references/navigation-and-javascript.md.
Local Content and Custom URL Schemes
Use WebPage.Configuration and URLSchemeHandler when the app needs bundled HTML, offline documents, or app-provided resources under a custom scheme.
var configuration = WebPage.Configuration()
configuration.urlSchemeHandlers[URLScheme("docs")!] = DocsSchemeHandler(bundle: .main)
let page = WebPage(configuration: configuration)
for try await _ in page.load(URL(string: "docs://article/welcome")!) {
}
Use this for:
- bundled documentation or article content
- offline HTML/CSS/JS assets
- app-owned resource loading under a custom scheme
Do not overuse custom schemes for normal remote content. Prefer standard HTTPS for server-hosted pages.
See references/local-content-and-custom-schemes.md.
WebView Customization
Use WebView modifiers to match the intended browsing experience.
Useful modifiers and related APIs:
webViewBackForwardNavigationGestures(_:)findNavigator(isPresented:)webViewScrollPosition(_:)webViewOnScrollGeometryChange(...)
Apply them only when the user experience needs them.
- Enable back/forward gestures when people are likely to visit multiple pages.
- Add Find in Page when the content is document-like.
- Sync scroll position only when the app has a sidebar, table of contents, or other explicit navigation affordance.
Apple’s HIG also applies here: support back/forward navigation when appropriate, but do not turn an app web view into a general-purpose browser.
Common Mistakes
- Using
WKWebViewwrappers by default in an iOS 26+ SwiftUI app instead of starting withWebViewandWebPage - Using embedded web views for OAuth instead of
ASWebAuthenticationSession - Reaching for
WebPageonly after building a plainWebView(url:)path that now needs state, JS, or navigation control - Treating
callJavaScriptas a direct replacement forWKScriptMessageHandler - Passing a callable JavaScript wrapper to
callJavaScriptinstead of only the function body - Iterating
page.navigationswithouttry/catcheven though navigation failure terminates the sequence by throwing - Binding the same
WebPageto multiple visibleWebViewvalues - Keeping all links inside the app when external domains should open outside the embedded surface
- Treating
SFSafariViewControlleras the cross-platform browse-out answer on macOS or visionOS instead of using default-browser/openURL behavior - Building a browser-style app shell around WebView instead of a focused embedded experience
- Using custom URL schemes for content that should just load over HTTPS
- Forgetting that
WebPageis main-actor-isolated
Review Checklist
- [ ]
WebViewandWebPageare the default path for iOS 26+ SwiftUI web content - [ ]
ASWebAuthenticationSessionis used for auth flows instead of embedded web views - [ ]
WebPageis used whenever the app needs state observation, JS calls, or policy control - [ ] Navigation policies only intercept the URLs the app actually owns or needs to reroute
- [ ] External domains open externally when appropriate
- [ ] JavaScript return values are cast defensively to concrete Swift types
- [ ]
callJavaScriptuses a function body and passes Swift values througharguments - [ ]
page.navigationsloops usefor try awaitand handle thrown navigation errors - [ ] Each visible
WebView(page)owns a distinctWebPage - [ ] Custom URL schemes are used only for real app-owned resources
- [ ] Back/forward gestures or controls are enabled when multi-page browsing is expected
- [ ]
SFSafariViewControlleris limited to iOS/iPadOS Safari-style modal browsing; macOS and visionOS browse-out flows use platform default browser behavior - [ ] The web experience adds focused native value instead of behaving like a thin browser shell
- [ ] Fallback to
WKWebViewis justified by deployment target or missing API needs
References
- Loading and observation: references/loading-and-observation.md
- Navigation and JavaScript: references/navigation-and-javascript.md
- Local content and custom schemes: references/local-content-and-custom-schemes.md
- Migration and fallbacks: references/migration-and-fallbacks.md
{
"skill_name": "swiftui-webkit",
"evals": [
{
"id": 0,
"name": "navigation-and-javascript-corrections",
"prompt": "Review this SwiftUI WebKit helper before I paste it into an iOS 26 app. It watches `page.navigations` with `for await`, switches on `.finished`, `.failed`, and `.failedProvisionalNavigation`, and builds JavaScript by interpolating a selected section ID into `function measure() { return document.getElementById('\\(id)').getBoundingClientRect().top } measure()`. What should I change?",
"expected_output": "A focused review that corrects WebPage navigation observation and callJavaScript usage without rewriting unrelated app architecture.",
"files": [],
"expectations": [
"Uses `for try await` for `page.navigations` and handles thrown navigation errors with `catch`.",
"Does not switch on undocumented `.failed` or `.failedProvisionalNavigation` `NavigationEvent` cases; treats failures as `WebPage.NavigationError` from the throwing sequence.",
"Explains that `callJavaScript` takes a function body rather than a wrapped function declaration or appended call expression.",
"Recommends passing the selected section ID through `arguments` instead of string interpolation.",
"Mentions casting JavaScript results defensively and handling `nil` or `NSNull` when relevant."
]
},
{
"id": 1,
"name": "container-boundary-routing",
"prompt": "I have one SwiftUI app with bundled help articles, a GitHub OAuth login, public legal links, a macOS build, a visionOS build, and one iOS screen that needs to inspect page title/progress and run JavaScript. Which web container should each part use?",
"expected_output": "A platform-aware container selection answer that keeps embedded WebKit, Safari-style browsing, auth, and fallback boundaries separate.",
"files": [],
"expectations": [
"Uses `WebView` plus `WebPage` for embedded app-owned content that needs page state, JavaScript, loading control, or navigation policy.",
"Uses `ASWebAuthenticationSession` for OAuth instead of `WebView`, `WKWebView`, or `SFSafariViewController`.",
"Uses `SFSafariViewController` only for iOS/iPadOS Safari-style modal browsing where the app does not need to interact with the content.",
"Routes macOS and visionOS browse-out behavior to `openURL` or the default browser instead of treating `SFSafariViewController` as the cross-platform answer.",
"Mentions `WKWebView` as a fallback only for back-deployment below iOS 26 or a missing required SwiftUI-facing capability."
]
},
{
"id": 2,
"name": "wkwebview-migration-review",
"prompt": "Review a migration plan for an existing SwiftUI feature that wraps `WKWebView` in `UIViewRepresentable`. It handles article pages, custom callback URLs from JavaScript, OAuth, and one heavily customized legacy browser screen. Product wants a modern iOS 26 plan but cannot afford a big rewrite.",
"expected_output": "An incremental migration review that moves routine embedded content to WebView/WebPage while preserving justified fallback and sibling boundaries.",
"files": [],
"expectations": [
"Recommends migrating routine article/detail content to `WebView` and `WebPage` when the new API covers the need.",
"Keeps OAuth on `ASWebAuthenticationSession` and does not move it into embedded WebKit.",
"Frames callback-URL interception as a coarse JS-to-native workaround, not a full `WKScriptMessageHandler` replacement.",
"Keeps the heavily customized legacy browser screen on an explicit `WKWebView` fallback if required capability gaps or rewrite cost justify it.",
"Recommends screen-by-screen migration and separation of page ownership, navigation policy, JavaScript calls, and UI embedding."
]
}
]
}
Loading and Observation
Contents
- Simple
WebView(url:) - Controlled
WebPageloading - Observing progress and title
- Observing navigation events
- Ephemeral pages and custom user agents
Simple WebView(url:)
Use WebView(url:) when the app only needs to display a URL and does not need explicit page control.
import SwiftUI
import WebKit
struct MarketingPageView: View {
let url: URL
var body: some View {
WebView(url: url)
.webViewBackForwardNavigationGestures(.enabled)
}
}This is the lowest-friction path for embedded content.
Controlled WebPage loading
Create a WebPage when the app needs to drive loading itself.
Keep page ownership one-to-one with presentation: a WebPage can be bound to only one visible WebView at a time.
@Observable
@MainActor
final class ArticleModel {
let page = WebPage()
var lastError: String?
func load(_ url: URL) async {
do {
for try await _ in page.load(URLRequest(url: url)) {
}
} catch {
lastError = error.localizedDescription
}
}
}struct ArticleDetailView: View {
@State private var model = ArticleModel()
let url: URL
var body: some View {
WebView(model.page)
.task {
await model.load(url)
}
}
}You can also load:
for try await _ in page.load(url) { }for try await _ in page.load(html: htmlString, baseURL: baseURL) { }for try await _ in page.load(data, mimeType: "text/html", characterEncoding: "utf-8", baseURL: baseURL) { }
Use the async sequence returned by a load call when you need to track that specific programmatic navigation. Use page.navigations for a broader stream covering both user and programmatic navigations.
Observing progress and title
WebPage is observable, so SwiftUI can bind directly to its state.
struct ReaderView: View {
@State private var page = WebPage()
var body: some View {
WebView(page)
.navigationTitle(page.title ?? "Loading")
.overlay(alignment: .top) {
if page.isLoading {
ProgressView(value: page.estimatedProgress)
.padding()
}
}
.task {
do {
for try await _ in page.load(URLRequest(url: URL(string: "https://example.com/docs")!)) {
}
} catch {
// Handle load failure.
}
}
}
}Useful properties:
titleurlisLoadingestimatedProgressthemeColorhasOnlySecureContentbackForwardList
Observing navigation events
Use currentNavigationEvent for a lightweight current-state view. Use navigations to observe the full sequence of navigation events.
@MainActor
func observeNavigations(for page: WebPage) {
Task {
do {
for try await event in page.navigations {
switch event {
case .startedProvisionalNavigation:
print("Navigation started")
case .receivedServerRedirect:
print("Navigation redirected")
case .committed:
print("Navigation committed")
case .finished:
print("Navigation finished")
@unknown default:
break
}
}
} catch {
print("Navigation failed: \(error)")
}
}
}This is the right place to trigger follow-up work like parsing headings after a finished navigation. Treat thrown errors as normal navigation failures such as invalid URLs, provisional navigation failures, page closure, or web content process termination.
Ephemeral pages and custom user agents
Use WebPage.Configuration when you need a nonpersistent page, custom user agent, or tighter loading rules.
@MainActor
func makeMetadataPage() -> WebPage {
var configuration = WebPage.Configuration()
configuration.loadsSubresources = false
configuration.defaultNavigationPreferences.allowsContentJavaScript = false
configuration.websiteDataStore = .nonPersistent()
let page = WebPage(configuration: configuration)
page.customUserAgent = "MetadataBot/1.0"
return page
}Use nonpersistent pages when you want an isolated web session or metadata fetch path without shared cookies or long-lived website data.
Local Content and Custom URL Schemes
Contents
- When to use custom schemes
- Registering a scheme handler
- Returning responses and data
- Loading bundled content
- Cancellation behavior
When to use custom schemes
Use URLSchemeHandler when the app owns the content source and needs WebKit to resolve resources under a custom scheme.
Good fits:
- bundled HTML, CSS, and JavaScript assets
- offline documentation
- app-owned rich content assembled on device
Do not use custom schemes for ordinary server-hosted pages that should just load over HTTPS.
Registering a scheme handler
import Foundation
import WebKit
@MainActor
func makeDocsPage() -> WebPage {
var configuration = WebPage.Configuration()
configuration.urlSchemeHandlers[URLScheme("docs")!] = DocsSchemeHandler(bundle: .main)
return WebPage(configuration: configuration)
}If WebKit already owns a scheme, URLScheme("https") style registration does not work. Use a genuinely custom scheme.
Returning responses and data
A handler replies with an async sequence of intermixed response and data values.
struct DocsSchemeHandler: URLSchemeHandler {
let bundle: Bundle
func reply(for request: URLRequest) -> some AsyncSequence<URLSchemeTaskResult> {
AsyncStream { continuation in
guard let url = request.url,
let fileURL = bundle.url(forResource: url.host, withExtension: "html", subdirectory: "Docs")
else {
continuation.finish()
return
}
do {
let data = try Data(contentsOf: fileURL)
let response = URLResponse(
url: url,
mimeType: "text/html",
expectedContentLength: data.count,
textEncodingName: "utf-8"
)
continuation.yield(.response(response))
continuation.yield(.data(data))
continuation.finish()
} catch {
continuation.finish()
}
}
}
}Keep MIME type and encoding aligned with the actual content you serve.
Loading bundled content
Once registered, load the custom URL like any other page.
let page = makeDocsPage()
for try await _ in page.load(URL(string: "docs://welcome")!) {
}
This works well when the HTML references other assets with the same custom scheme.
Cancellation behavior
If WebKit no longer needs the resource, it cancels the task producing the async sequence. Treat cancellation as normal behavior when:
- the user navigates away
- the page reloads before the previous request completes
- the resource is no longer needed by the page
Do not build logic that assumes every scheme-handled request runs to completion.
Migration and Fallbacks
Contents
- Migration goal
- Decision guide
- Migrating from
WKWebViewwrappers - Incremental migration pattern
- When
SFSafariViewControlleris still the right choice - When
ASWebAuthenticationSessionis required - When
WKWebViewremains justified - Testing the migration
- Review checklist
Migration goal
For SwiftUI apps targeting iOS 26+, the default for routine embedded web content should be native SwiftUI WebKit APIs (WebView, WebPage) instead of a custom UIViewRepresentable wrapper around WKWebView.
The goal is not "delete every WKWebView immediately." The goal is to move routine embedded web content to the new native surface and keep legacy fallback paths only where they are still justified.
Decision guide
| Use case | Best default |
|---|---|
| Embedded app-owned web content in a SwiftUI screen | WebView + WebPage |
| OAuth or third-party sign-in | ASWebAuthenticationSession |
| External public site with Safari behavior on iOS/iPadOS | SFSafariViewController |
| External public site browse-out on macOS or visionOS | openURL / default browser |
| Back-deploying below iOS 26 or missing required capability | WKWebView fallback |
Migrating from WKWebView wrappers
For SwiftUI apps targeting iOS 26+, start from WebView and WebPage for routine embedded content instead of a UIViewRepresentable wrapper around WKWebView.
Typical mapping:
| Older pattern | Modern default |
|---|---|
UIViewRepresentable wrapper for WKWebView | WebView(url:) or WebView(page) |
WKNavigationDelegate policy handling | WebPage.NavigationDeciding |
KVO for title, url, or loading state | observable WebPage properties |
evaluateJavaScript | callJavaScript |
custom WKWebViewConfiguration usage | WebPage.Configuration |
Migrate first when the app is already SwiftUI-native and only kept WKWebView because there was no native view before.
Incremental migration pattern
A clean migration is usually screen-by-screen, not all-at-once.
Start by separating page ownership from view ownership
If the current wrapper mixes:
- page state
- navigation policy
- JS calls
- UI embedding
split those concerns first. Once page logic is no longer trapped inside the wrapper, moving to WebPage gets much easier.
Move the simplest screens first
Best first migrations:
- static help center content
- app-owned account or legal pages
- embedded flows that only need URL loading, title, and progress
Leave these for later:
- auth flows
- highly customized legacy UIKit screens
- surfaces relying on a capability you have not yet mapped to the new API set
Keep fallback boundaries explicit
Use availability and architecture boundaries instead of mixing two approaches in one view body.
struct HelpCenterScreen: View {
let page = WebPage()
var body: some View {
WebView(page)
.task {
try? await page.load(URLRequest(url: helpCenterURL))
}
}
}If a fallback is still needed, isolate it in a separate type rather than sprinkling if #available checks through the screen's core logic.
When SFSafariViewController is still the right choice
Use SFSafariViewController on iOS and iPadOS when the app just needs to show an external site with Safari behavior and does not need page-level control.
Good fits:
- help center article from a public website
- a legal page or blog post
- a temporary browse-out flow that should keep Safari chrome and reader behavior
Do not use SFSafariViewController when the app needs to:
- observe page state
- run JavaScript
- intercept navigation
- coordinate in-app page history
On macOS and visionOS, prefer platform default-browser behavior through openURL instead of treating SFSafariViewController as the browse-out surface.
When ASWebAuthenticationSession is required
Use ASWebAuthenticationSession for OAuth and third-party sign-in.
This remains true even if the rest of the app uses WebView for embedded content.
Do not replace auth sessions with embedded web views. The authentication skill owns that flow because the product requirement is secure sign-in, not generic web content.
When WKWebView remains justified
A fallback WKWebView path can still make sense when:
- the app must back-deploy below iOS 26
- the codebase is still UIKit-first and not ready to move the surface into
native SwiftUI WebKit APIs
- a required legacy-only WebKit capability is not yet available through the new
SwiftUI-facing API surface
- a heavily customized existing surface would create churn without product value
When you keep WKWebView, treat it as a deliberate fallback, not the default architecture for a modern iOS 26+ SwiftUI feature.
Testing the migration
For each migrated screen, verify:
- title and URL state still update correctly
- JavaScript calls still reach the page when needed
- navigation policy behavior still matches product expectations
- loading and error states still render at the right time
- no auth flow accidentally moved from
ASWebAuthenticationSessionto an
embedded view
- fallback
WKWebViewscreens stay isolated instead of spreading wrapper logic
back into new screens
Review checklist
- [ ] New SwiftUI-native screens default to
WebViewandWebPage - [ ]
SFSafariViewControllerused only for browse-out Safari-style flows - [ ]
ASWebAuthenticationSessionretained for OAuth and sign-in - [ ]
WKWebViewkept only where back-deployment or capability gaps justify it - [ ] Fallback paths isolated to dedicated wrapper types
- [ ] No new iOS 26+ feature starts from a
UIViewRepresentablewrapper by default
Navigation and JavaScript
Contents
- Navigation policy decisions
- Opening external links outside the embedded web view
- Calling JavaScript
- Passing arguments into JavaScript
- Coarse JS-to-native signaling
Navigation policy decisions
Use WebPage.NavigationDeciding when the app needs to allow only owned URLs inside the embedded page.
import Observation
import WebKit
@Observable
@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 == "docs.example.com" {
return .allow
}
urlToOpenExternally = url
return .cancel
}
}This keeps app-owned pages embedded while still letting the app hand off external destinations.
Opening external links outside the embedded web view
Bridge the decider back into SwiftUI with openURL.
struct ArticleView: View {
@Environment(\.openURL) private var openURL
@State private var decider: ArticleNavigationDecider
@State private var page: WebPage
init() {
let decider = ArticleNavigationDecider()
_decider = State(initialValue: decider)
_page = State(initialValue: WebPage(configuration: .init(), navigationDecider: decider))
}
var body: some View {
WebView(page)
.onChange(of: decider.urlToOpenExternally) { _, url in
guard let url else { return }
openURL(url)
decider.urlToOpenExternally = nil
}
}
}Use this for external links, legal pages, or routes that should leave the embedded surface.
Calling JavaScript
callJavaScript executes an async JavaScript function and returns an optional Any. Pass only the function body. Do not wrap the script in function foo() { ... } or append a call expression.
let script = """
const headings = [...document.querySelectorAll('h1, h2')];
return headings.map(node => ({
id: node.id,
title: node.textContent?.trim()
}));
"""
let result = try await page.callJavaScript(script)
let headings = result as? [[String: Any]] ?? []Cast immediately into the specific structure the app expects. If the function body has no explicit return, the result is nil; if JavaScript explicitly returns null, handle NSNull.
Passing arguments into JavaScript
Arguments become local variables inside the JavaScript function. Use them for Swift-provided values instead of interpolating values into the script string. Supported values include numbers, strings, dates, and arrays, dictionaries, and optionals of those value types.
let topOffset = try await page.callJavaScript(
"return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;",
arguments: ["sectionID": selectedSectionID]
) as? DoubleThis is cleaner than interpolating untrusted values into the script string.
Coarse JS-to-native signaling
The native SwiftUI WebKit API clearly supports Swift-to-JavaScript calls, but it does not expose an obvious direct equivalent to WKScriptMessageHandler.
For coarse event handoff, a custom navigation pattern can work:
- JavaScript navigates to a custom callback URL like
app-event://completed?id=123 NavigationDecidingintercepts the URL- the decider extracts data and returns
.cancel
Use this for simple completion or routing signals. Do not present it as a full structured messaging replacement for legacy WKUserContentController script handlers, and keep richer native/web messaging on a WKWebView fallback until the SwiftUI-facing API covers the need.
Related skills
FAQ
What is swiftui-webkit?
Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, JavaScript execution, observable page state, link interception,
When should I use swiftui-webkit?
Embeds and controls web content in SwiftUI with WebKit for SwiftUI, including WebView, WebPage, navigation policies, JavaScript execution, observable page state, link interception,
Is swiftui-webkit safe to install?
Review the Security Audits panel on this page before production use.