# Incoming Links and Handoff

Part of the `swiftui-navigation` skill. The aim is to send external URLs to
the right screen inside the app, and to let the system handle anything the app
does not own.

## Shape of the solution

- All URL handling lives in router methods, here `handle(url:)` and
  `handleDeepLink(url:)`.
- The environment's `openURL` is replaced by an `OpenURLAction` that asks the
  router first.
- `.onOpenURL` receives app-scheme links and turns them into web URLs when the
  router expects those.
- The router alone decides between navigating in the app and opening the URL
  outside it.

## Router entry points

```swift
@MainActor @Observable
final class Router {
    var fallback: ((URL) -> OpenURLAction.Result)?
    var path: [Route] = []

    func push(_ next: Route) { path.append(next) }

    func handle(url: URL) -> OpenURLAction.Result {
        guard let route = Route(url: url) else {
            return fallback.map { $0(url) } ?? .systemAction
        }
        path.append(route)
        return .handled
    }

    func handleDeepLink(url: URL) -> OpenURLAction.Result {
        Task {
            guard let resolved = await LinkResolver.shared.canonical(for: url),
                  let route = Route(url: resolved) else { return }
            path.append(route)
        }
        return .handled
    }
}
```

`handle(url:)` navigates for URLs the app understands and otherwise passes the
URL on, ending with the system default. `handleDeepLink(url:)` first resolves
links that point at another server (for example a federated or shortened URL),
then navigates. Resolution runs in a `Task` so the UI never waits on the
network.

## Installing it once at the top

```swift
extension View {
    func withLinkRouter(_ links: Router) -> some View {
        onOpenURL { incoming in _ = links.handleDeepLink(url: incoming) }
            .environment(\.openURL, OpenURLAction(handler: links.handle(url:)))
    }
}
```

## Rules and pitfalls

- Parsing and the decision to navigate live in the router, behind one entry
  point.
- Always fall back to the environment's `openURL` behaviour through
  `OpenURLAction` so unknown links still open.
- Check that a URL really belongs to the app before treating it as internal.
- Never block the interface while a remote link resolves; use a `Task`.

## Universal links

Universal links need server configuration plus the Associated Domains
entitlement, and no custom scheme.

Put the association file under the domain's `.well-known` folder, at
`https://yourdomain.example/.well-known/apple-app-site-association`. The name
has no extension, and the response header should say JSON
(`Content-Type: application/json`):

```json
{
  "applinks": {
    "defaults": { "caseSensitive": false },
    "details": [
      {
        "appIDs": ["A1B2C3D4E5.org.sample.hiker"],
        "components": [
          { "/": "/trail/*", "comment": "Trail pages" },
          { "/": "/ranger/*" }
        ]
      }
    ]
  }
}
```

- HTTPS only, a certificate that validates, and no redirect on the way.
- The system fetches the file through a CDN cache, so changes can take 24 to 48
  hours to arrive. To see what devices currently get, open
  `https://app-site-association.cdn-apple.com/a/v1/yourdomain.example`.
- Write rules with `components`; the older `paths` key is legacy.

Entitlement (`com.apple.developer.associated-domains`):

```xml
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:yourdomain.example</string>
    <string>applinks:www.yourdomain.example</string>
</array>
```

While developing, `applinks:yourdomain.example?mode=developer` skips the CDN
cache.

Reference: [universal links setup guide](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)

## Custom URL schemes

A scheme such as `hiker://` needs no server, but if the app is missing the link
simply fails.

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
        <key>CFBundleURLName</key>
        <string>org.sample.hiker</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>hiker</string>
        </array>
    </dict>
</array>
```

```swift
.onOpenURL { url in
    guard url.scheme == "hiker" else { return }
    switch url.host {
    case "trail":
        if let id = url.pathComponents.dropFirst().first {
            router.push(.trail(id: id))
        }
    default:
        break
    }
}
```

For links people share in public, universal links are the better experience,
because they fall back to the website, and the safer one, because the domain
is verified.

## Handoff with NSUserActivity

Handoff lets someone start on one device and carry on from another.

```swift
struct TrailDetailView: View {
    @Environment(Router.self) private var router
    let trail: Trail

    var body: some View {
        TrailSummary(trail: trail)
            .userActivity("org.sample.hiker.viewTrail") { current in
                current.title = trail.name
                current.targetContentIdentifier = trail.id
                current.webpageURL = URL(string: "https://yourdomain.example/trail/\(trail.id)")
                current.isEligibleForSearch = true
                current.isEligibleForHandoff = true
            }
    }
}

struct RootView: View {
    @Environment(Router.self) private var nav

    var body: some View {
        MainTabs()
            .onContinueUserActivity("org.sample.hiker.viewTrail") { resumed in
                guard let trailID = resumed.targetContentIdentifier else { return }
                nav.push(.trail(id: trailID))
            }
    }
}
```

- List every activity type in the Info.plist `NSUserActivityTypes` array.
- `isEligibleForSearch` and `isEligibleForPrediction` are optional extras.
- `webpageURL` is where the other device goes when it does not have the app.
