A small Swift package for routing incoming URLs (custom-scheme deep links and Universal Links alike) into typed SwiftUI navigation, without hand-rolling navigation plumbing for each new screen.
- Turns an incoming URL into an app-defined
Intentvia yourURLParsingcodec. - A
NavigationFlowtranslates each intent into a list ofStepvalues:Step.nav(.push/.present/.popToRoot/.dismissSheet): structural navigation, dispatched by Iris.Step.effect(_): consumer-defined side-effects, handled in your coordinator'sapply(_:_:).
PlumbedCoordinatorBase<Flow>ships the navigators, facade, executors, and stack/sheetHandoffRegistrys out of the box; subclasses just declare theirFlowand overrideapply(_:_:)when there are effects to handle.- Latest-wins cancellation so a newer link supersedes one in flight.
- Hand-off batons let destination views consume the link payload once
the screen mounts; the registry auto-cleans on
.delivered.
Iris is the package behind Deep linking as an input language, a series of measurement-led writeups on the problems it exists to solve: where URL handling should live, resolving the same URL to the right meaning from anywhere in the app, keeping taps and URLs on one resolver, and what happens when links arrive faster than the UI can mount them. Iris internals: how it works walks the pipeline this package implements.
- iOS 17+ or macOS 14+
- Swift 6.0 / Xcode 16+
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/async-digital-ltd/iris.git", from: "1.0.0")
]…and depend on the Iris product from any target that needs it.
// 1. Define your flow.
enum MyFlow: NavigationFlow {
typealias Intent = MyIntent
typealias Route = TopLevel.StackRoute
typealias SheetRoute = TopLevel.SheetRoute
static func operations(intent: Intent) -> [Step<Route, SheetRoute, Never>] {
switch intent {
case .showInbox:
return [.nav(.popToRoot), .nav(.push(.inbox))]
case .showProfile(let id):
return [.nav(.popToRoot), .nav(.push(.profile(id: id)))]
case .unknown:
return []
}
}
}
// 2. Subclass PlumbedCoordinatorBase.
@MainActor @Observable
final class AppCoordinator: PlumbedCoordinatorBase<MyFlow> {}
// 3. Drive NavigationStack from the inherited facade.
NavigationStack(path: coordinator.nav.pathBinding) {
RootView()
.navigationDestination(for: TopLevel.StackRoute.self) { route in
// …
}
}
.sheet(item: coordinator.nav.sheetBinding) { route in
// …
}Sources/Iris/Excluded/Skills/ holds five SKILL.md templates aimed
at coding agents (excluded from the SwiftPM target). They scaffold a new
consumer end to end:
iris-bootstrap: generates the Intent / routes / flow / codec / coordinator / app-entry wiring.iris-test-scaffold: generates Swift Testing suites for codec, flow, and handoff lifecycle.iris-audit: runs eight wiring-coverage checks against an existing consumer.iris-visualize: produces a Mermaid diagram of the URL → Intent → Step → Route → View pipeline.iris-url-catalog: produces a Markdown table of every supported URL.
Iris follows Semantic Versioning. The public API is stable and will not break within a major version: no source-breaking change ships in a minor or patch release. Anything that would break a consumer waits for the next major version and is documented in that release's notes.
The promise covers the public surface of the Iris product. Internal
symbols and the templates under Sources/Iris/Excluded/ are outside it and
may change at any time.
Issues are welcome: bug reports, questions about wiring a host app, and proposals for changes all belong in the issue tracker.
Please open an issue before opening a pull request. Agreeing the shape of a change first avoids work that cannot be merged, and keeps the public API promise above intact.
Iris is available under the MIT License. See LICENSE for the full text.