All skills
avdlee avatar

/swiftui-expert-skill

@1e522cf

Use when writing, reviewing, or refactoring SwiftUI code for iOS or macOS, including state and `@Observable` data flow, view composition, resizable layouts, safe areas, display scale, performance, lists, environment, localization, animation, Liquid Glass, and API migration. Also use for iPhone Duo, foldable, or large-display layouts (`NavigationSplitView` on large displays, two-column reflow, foldable grids, `ArrangementView`, `ReservedRegion`), hinge effects, vertical bars, `@State` initialization or synthesized-property diagnostics, `@ContentBuilder` ambiguity, `reorderable` drag/drop, custom `AsyncImage` `URLSession`, swipe actions outside List, item-bound `alert`/`confirmationDialog`, `ToolbarOverflowMenu`, `AnimatableValues`, Document APIs (`Document`/`DocumentReader`), and Instruments `.trace` capture or analysis.

Use this Skill: https://skilld.dev/gh/avdlee/swiftui-agent-skill/swiftui-expert-skill

This session only. Nothing lands on disk.

referenceswebkit-integration.md

≈2.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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 has two initializers: one for a bare URL, one backed by a WebPage.

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.

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.

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.

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.

_ = 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:

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).

.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.

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

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:

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:

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:

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.

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.

Source: SKILL.md on GitHub

No alerts2d5 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    The skill is a professional-grade assistant for SwiftUI development and performance profiling. It includes Python scripts to interface with the Xcode xctrace CLI for recording and analyzing Instruments traces. All identified code and instructions are consistent with its stated purpose and follow security best practices.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: LOW · No issues

  • Runlayer6mo

    19 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1e522cf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 days ago
  • Performance
  • swiftui
  • ios
  • macos
  • instruments
  • state-management
  • view-composition
  • accessibility
  • animations

README badge

README badge for avdlee/swiftui-agent-skill/swiftui-expert-skill

Provides guidance for writing, reviewing, and refactoring SwiftUI code for iOS and macOS, including state management, view composition, performance optimization, and Instruments trace recording and analysis. Covers deprecated API detection, animation patterns, accessibility, and Liquid Glass adoption.

Generated from the current SKILL.md.

Does this skill work with both iOS and macOS?
Yes. The skill covers SwiftUI for both iOS and macOS, with dedicated reference sections for macOS-specific patterns like scenes, window styling, and views (HSplitView, Table, PasteButton).
Can this skill help me record and analyze Instruments traces?
Yes. The skill includes workflows to record traces via `record_trace.py` (with template selection for real devices vs simulators) and analyze them via `analyze_trace.py` to identify hangs, hitches, CPU hotspots, and excessive view updates.
Does this skill enforce a specific architecture pattern?
No. It focuses on correctness and performance without mandating MVVM, VIPER, or other architectural styles, though it encourages separating business logic from views for testability.
What does the skill do about deprecated APIs?
It consults `references/latest-apis.md` at the start of every task to identify and replace deprecated APIs with modern equivalents across iOS 15+ through iOS 26+, and gates version-specific APIs with `#available`.
Does this skill handle Liquid Glass effects?
Yes, but only when explicitly requested by the user. It includes guidance in `references/liquid-glass.md` for iOS 26+ Liquid Glass adoption with sensible fallbacks for earlier versions.

Generated from the current SKILL.md. These answers refresh after source changes.