# Migration and fallbacks

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

## What the migration is for

In a SwiftUI app targeting iOS 26 or later, `WebView` and `WebPage` are the
starting point for embedded web content. The aim is to move everyday screens
onto them and keep only the fallbacks that still earn their place, not to
delete every `WKWebView` in one sweep.

## Choosing a container

| Need | Choice |
|---|---|
| App-owned web content inside SwiftUI | `WebView` + `WebPage` |
| OAuth or third-party sign-in | `ASWebAuthenticationSession` |
| An outside site that should behave like Safari | `SFSafariViewController` |
| Older deployment target, or a capability the new API lacks | `WKWebView` |

## Mapping the old pieces

| `WKWebView` era | SwiftUI WebKit |
|---|---|
| `UIViewRepresentable` wrapping `WKWebView` | `WebView(url:)` or `WebView(page)` |
| `WKNavigationDelegate` policy methods | `WebPage.NavigationDeciding` |
| KVO on `title`, `url`, `isLoading` | observable properties on `WebPage` |
| `evaluateJavaScript(_:)` | `callJavaScript(_:arguments:in:contentWorld:)` |
| `WKWebViewConfiguration` | `WebPage.Configuration` |

The strongest case for moving first is a SwiftUI-native app that kept a
`WKWebView` wrapper only because no SwiftUI view existed.

## Moving one screen at a time

1. Inside the old wrapper, separate four concerns: page state, navigation
   policy, JavaScript calls and the view embedding.
2. Move the screens with the least risk first: a static help center, account
   or legal pages the app owns, flows that only need URL, title and progress.
3. Leave for later: sign-in flows, heavily customised UIKit screens, and
   anything that depends on a capability the new API does not map.
4. Keep remaining fallbacks in their own wrapper type instead of scattering
   `if #available` checks through screen logic.

```swift
struct EmbeddedDocument: View {
    let address: URL

    var body: some View {
        if #available(iOS 26.0, *) {
            WebView(url: address)
        } else {
            LegacyDocumentView(address: address)   // the one place WKWebView lives
        }
    }
}
```

## When SFSafariViewController is still right

Use it to send people to an outside site while keeping Safari's chrome and
Reader: a public help article, a legal page hosted elsewhere, a blog post.
It is the wrong tool once the app needs page state, JavaScript, navigation
interception or its own history handling.

## When ASWebAuthenticationSession is required

Every OAuth or third-party sign-in flow goes through it, even when the rest
of the feature uses `WebView`. The `authentication` skill covers the flow
itself.

## When WKWebView still makes sense

- the deployment target is below iOS 26
- a UIKit-first codebase is not ready to move
- a capability exists only on the old API
- the screen is so customised that migrating it costs more than it returns

Keep such cases as a conscious fallback, documented where the wrapper lives.

## Testing a migrated screen

- Title and URL update as the user navigates.
- JavaScript calls reach the page and their results decode.
- Policy decisions match the old delegate (same links stay in, same links leave).
- Loading and error states appear at the right moments.
- No sign-in flow ended up inside the embedded view.
- Fallback wrappers are still confined to their own types.

## Checklist

- [ ] New screens start with `WebView` and `WebPage`
- [ ] `SFSafariViewController` is used only for browse-out
- [ ] Sign-in stays on `ASWebAuthenticationSession`
- [ ] `WKWebView` appears only where one of the reasons above applies
- [ ] Fallbacks live in dedicated wrapper types
- [ ] No new iOS 26+ feature begins life as a `UIViewRepresentable` web view wrapper
