# Navigation and JavaScript

Part of the `swiftui-webkit` skill ([../SKILL.md](../SKILL.md)).

## An observable navigation decider

Making the decider itself `@Observable` lets a view react when it cancels a
navigation that should leave the app.

```swift
import Observation
import WebKit

@Observable
@MainActor
final class LinkPolicy: WebPage.NavigationDeciding {
    var outboundURL: URL?
    private let ownedHosts: Set<String> = ["support.example.net", "status.example.net"]

    func decidePolicy(for step: WebPage.NavigationAction,
                      preferences prefs: inout WebPage.NavigationPreferences)
        async -> WKNavigationActionPolicy {
        guard let target = step.request.url else { return .allow }
        if let host = target.host(), ownedHosts.contains(host) { return .allow }
        if target.scheme == "about" { return .allow }
        outboundURL = target
        return .cancel
    }
}
```

## Sending outside links to the system

Build the decider and the page once, in `init`, then open whatever the
decider rejected with the environment's `openURL`.

```swift
import SwiftUI
import WebKit

struct SupportScreen: View {
    @Environment(\.openURL) private var openURL
    @State private var policy: LinkPolicy
    @State private var page: WebPage
    let entry: URL

    init(entry: URL) {
        self.entry = entry
        let policy = LinkPolicy()
        _policy = State(wrappedValue: policy)
        let setup = WebPage.Configuration()
        _page = State(wrappedValue: WebPage(configuration: setup, navigationDecider: policy))
    }

    var body: some View {
        WebView(page)
            .task { _ = page.load(entry) }
            .onChange(of: policy.outboundURL) { _, destination in
                guard let destination else { return }
                openURL(destination)
                policy.outboundURL = nil
            }
    }
}
```

## Running JavaScript

The string passed to `callJavaScript` becomes the body of an async function,
and whatever it returns comes back as `Any?`. Convert it to the structure you
expect immediately so the rest of the code stays typed.

```swift
struct Outline {
    let anchor: String
    let label: String
}

@MainActor
func readOutline(of page: WebPage) async throws -> [Outline] {
    let outcome = try await page.callJavaScript("""
        return Array.from(document.querySelectorAll('h2[id]'), el => ({ anchor: el.id, label: el.innerText }));
        """)
    guard let rows = outcome as? [[String: Any]] else { return [] }
    return rows.compactMap { row in
        guard let anchor = row["anchor"] as? String, let label = row["label"] as? String else { return nil }
        return Outline(anchor: anchor, label: label)
    }
}
```

## Handing values to the script

Every key of the `arguments` dictionary turns into a local variable of that function. This is the
safe way to feed in user text or other untrusted values; never splice them
into the script string.

```swift
@MainActor
func highlightCount(of term: String, in page: WebPage) async throws -> Int {
    let found = try await page.callJavaScript(
        "return (document.body.innerText.match(new RegExp(needle, 'gi')) || []).length;",
        arguments: ["needle": NSRegularExpression.escapedPattern(for: term)]
    )
    return (found as? Double).map(Int.init) ?? 0
}
```

JavaScript numbers arrive as `Double` (or `NSNumber`), so cast accordingly.

## Simple signals from the page to the app

For a one-bit message such as "the form is done", the page can navigate to a
custom URL that the decider recognises, reads and cancels:

```javascript
window.location.href = "app-event://checkout-finished?order=A1942";
```

```swift
@MainActor
final class CheckoutWatcher: WebPage.NavigationDeciding {
    var finishedOrder: String?

    func decidePolicy(for step: WebPage.NavigationAction,
                      preferences prefs: inout WebPage.NavigationPreferences)
        async -> WKNavigationActionPolicy {
        guard let target = step.request.url, target.scheme == "app-event" else { return .allow }
        let parts = URLComponents(url: target, resolvingAgainstBaseURL: false)
        finishedOrder = parts?.queryItems?.first { $0.name == "order" }?.value
        return .cancel
    }
}
```

Keep this to simple completion or routing signals. It is not a structured
substitute for script message handlers registered on a
`WKUserContentController`; when you need real two-way messaging, use the
`userContentController` that `WebPage.Configuration` exposes.
