CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

navigation-and-javascript.mdskills/swiftui-webkit/references/

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.

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.

Passing arguments into JavaScript

Arguments become local variables inside the JavaScript function.

let topOffset = try await page.callJavaScript(
    "return document.getElementById(sectionID)?.getBoundingClientRect().top ?? null;",
    arguments: ["sectionID": selectedSectionID]
) as? Double

This 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
  • NavigationDeciding intercepts 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.

skills

CHANGELOG.md

README.md

tile.json