{"id":760,"url":"https://github.com/Bahn-X/swift-composable-navigator","last_synced_at":"2025-07-30T19:32:14.420Z","repository":{"id":40621435,"uuid":"308578832","full_name":"Bahn-X/swift-composable-navigator","owner":"Bahn-X","description":"An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind","archived":false,"fork":false,"pushed_at":"2022-02-18T20:36:35.000Z","size":9747,"stargazers_count":579,"open_issues_count":11,"forks_count":25,"subscribers_count":20,"default_branch":"main","last_synced_at":"2024-08-14T13:17:23.732Z","etag":null,"topics":["composition","ios","modularity","navigation","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Bahn-X.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null}},"created_at":"2020-10-30T09:12:04.000Z","updated_at":"2024-08-10T22:07:50.000Z","dependencies_parsed_at":"2022-09-15T09:11:59.278Z","dependency_job_id":null,"html_url":"https://github.com/Bahn-X/swift-composable-navigator","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bahn-X%2Fswift-composable-navigator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bahn-X%2Fswift-composable-navigator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bahn-X%2Fswift-composable-navigator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bahn-X%2Fswift-composable-navigator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bahn-X","download_url":"https://codeload.github.com/Bahn-X/swift-composable-navigator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228178885,"owners_count":17881104,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composition","ios","modularity","navigation","swiftui"],"created_at":"2024-01-05T20:15:30.734Z","updated_at":"2024-12-04T19:31:56.312Z","avatar_url":"https://github.com/Bahn-X.png","language":"Swift","funding_links":[],"categories":["App Routing","Navigation","SwiftUI"],"sub_categories":["Content","Getting Started"],"readme":"# Unmaintained \nSee [#80](https://github.com/Bahn-X/swift-composable-navigator/pull/80)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./Documentation/logo.png\" width=\"15%\" align-content: center/\u003e\n\u003c/p\u003e\n\u003ch3 align=\"center\"\u003eComposable Navigator\u003c/h2\u003e\n\u003ch3 align=\"center\"\u003e\n  An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind\n\u003c/h3\u003e\n\u003cp align=\"center\"\u003e\u003ca title=\"GitHub Actions\" target=\"_blank\" href=\"https://github.com/Bahn-X/swift-composable-navigator/workflows/test/badge.svg\"\u003e\n\t\t\u003cimg src=\"https://github.com/Bahn-X/swift-composable-navigator/workflows/test/badge.svg\"\n\t\t\t\t\talt=\"test status\"\u003e\u003c/p\u003e\n\u003chr class=\"rounded\"\u003e\n\n- [Vanilla SwiftUI navigation](#vanilla-swiftui-navigation)\n- [Challenges](#challenges)\n- [Why should I use ComposableNavigator?](#why-should-i-use-composablenavigator)\n- [Core components](#core-components)\n  - [Navigation Path](#navigation-path)\n  - [Navigator](#navigator)\n  - [NavigationTree](#navigationtree)\n- [Vanilla SwiftUI + ComposableNavigator](#vanilla-swiftui--composablenavigator)\n- [Integrating ComposableNavigator](#integrating-composablenavigator)\n- [Deeplinking](#deeplinking)\n- [Dependency injection](#dependency-injection)\n- [Installation](#installation)\n  - [Swift Package](#swift-package)\n  - [Xcode](#xcode)\n- [Example application](#example-application)\n- [Documentation](#documentation)\n- [Contribution](#contribution)\n- [License](#license)\n\n\u003chr class=\"rounded\"\u003e\n\n## Vanilla SwiftUI navigation\nA typical, vanilla SwiftUI application manages its navigation state (i.e. is a sheet or a push active) either directly in its Views or in ObservableObjects.\n\nLet's take a look at a simplified example in which we keep all navigation state locally in the view:\n\n```swift\nstruct HomeView: View {\n  @State var isSheetActive: Bool = false\n  @State var isDetailShown: Bool = false\n\n  var body: some View {\n    VStack {\n      NavigationLink(\n        destination: DetailView(),\n        isActive: $isDetailShown,\n        label: {\n          Text(\"Go to detail view\")\n        }\n      )\n\n      Button(\"Go to settings\") {\n        isSheetActive = true\n      }\n    }\n    .sheet(\n      isPresented: $isSheetActive,\n      content: {\n        SettingsView()\n      }\n    )\n  }\n}\n```\n\n## Challenges\n### How do we test that when the user taps the navigation link, we move to the DetailView and not the SettingsView?\u003c!-- omit in toc --\u003e\nAs `isSheetActive` and `isDetailShown` are kept locally in the View and their values are directly mutated by a binding, we cannot test any navigation logic unless we write UI tests or implement custom bindings that call functions in an ObservableObject mutating the navigation state.\n\n### What if I want to show a second sheet with different content?\u003c!-- omit in toc --\u003e\nWe can either introduce an additional `isOtherSheetActive` variable or a hashable enum `HomeSheet: Hashable` and keep track of the active sheet in a `activeSheet: HomeSheet?` variable.\n\n### What happens if both `isSheetActive` and `isDetailShown` are true?\u003c!-- omit in toc --\u003e\nThe sheet is shown on top of the current content, meaning that we can end up in a situation in which the settings sheet is presented on top of a detail view.\n\n### How do we programmatically navigate after a network request has finished?\u003c!-- omit in toc --\u003e\nTo programmatically navigate, we need to keep our navigation state in an ObservableObject that performs asynchronous actions such as network requests. When the request succeeds, we set `isDetailShown` or `isSheetActive` to true. We also need to make sure that all other navigation related variables are set to false/nil or else we might end up with an unexpected navigation tree.\n\n### What happens if the NavigationLink is contained in a lazily loaded List view and the view we want to navigate to has not yet been initialized?\u003c!-- omit in toc --\u003e\nThe answer to this one is simple: SwiftUI will not navigate. Imagine, we have a list of hundreds of entries that the user can scroll through. If we want to programmatically navigate to an entry detail view, the 'cell' containing the NavigationLink needs to be in memory or else the navigation will not be performed.\n\n### NavigationLinks do not navigate when I click them\u003c!-- omit in toc --\u003e\nIn order to make NavigationLinks work in our view, we need to wrap our view in a NavigationView.\n\nSo, at which point in the view hierarchy do we wrap our content in a NavigationView? As wrapping content in a NavigationView twice will lead to two navigation bars, we probably want to avoid having to multiple nested NavigationViews.\n\n### Shallow Deeplinking\u003c!-- omit in toc --\u003e\nVanilla SwiftUI only supports shallow deeplinking, meaning that we can navigate from the ExampleView to the DetailView by setting the initial value of `isDetailShown` to true. However, we cannot navigate further down into our application as SwiftUI seems to ignore initial values in pushed/presented views.\n\n## Why should I use ComposableNavigator?\n**ComposableNavigator** lifts the burden of manually managing navigation state off your shoulders and allows to navigate through applications along navigation paths. **ComposableNavigator** takes care of embedding your views in NavigationViews, where needed, and always builds a valid view hierarchy. On top of that, **ComposableNavigator** unlocks advanced navigation patterns like wildcards and conditional navigation paths.\n\n## Core components\n**ComposableNavigator** is built on three core components: the navigation tree, the current navigation path, and the navigator.\n\n### Navigation Path\nThe navigation path describes the order of visible screens in the  application. It is a first-class representation of the `\u003curl-path\u003e` defined in [RFC1738](https://tools.ietf.org/html/rfc1738#section-3.1). A navigation path consists of identified screens.\n\n#### Screen\u003c!-- omit in toc --\u003e\nA Screen is a first-class representation of the information needed to build a particular view. Screen objects identify the navigation path element and can contain arguments like IDs, initial values, and flags. `detail?id=0` directly translates to `DetailScreen(id: 0)`.\n\nScreens define how they are presented. This decouples presentation logic from business logic, as showing a sheet and pushing a view are performed by invoking the same `go(to:, on:)` function. Changing a screen's (default) presentation style is a single line change. Currently, sheet and push presentation styles are supported.\n\n### Navigator\nThe navigator manages the application's current navigation path and allows mutations on it. The navigator acts as an interface to the underlying data source. The navigator object is accessible via the view environment.\n\nNavigators allow programmatic navigation and can be injected where needed, even into ViewModels.\n\n### NavigationTree\nThe **ComposableNavigator** is based on the concept of `PathBuilder` composition in the form of a `NavigationTree`. A `NavigationTree`  composes `PathBuilder`s to describe all valid navigation paths in an application. That also means that all screens in our application are accessible via a pre-defined navigation path.\n\nLet's look at an example `NavigationTree`:\n\n```swift\nstruct AppNavigationTree: NavigationTree {\n  let homeViewModel: HomeViewModel\n  let detailViewModel: DetailViewModel\n  let settingsViewModel: SettingsViewModel\n\n  var builder: some PathBuilder {\n    Screen(\n      HomeScreen.self,\n      content: {\n        HomeView(viewModel: homeViewModel)\n      },\n      nesting: {\n        DetailScreen.Builder(viewModel: detailViewModel)\n        SettingsScreen.Builder(viewModel: settingsViewModel)\n      }\n    )\n  }\n}\n```\n\n![Example Tree](./Documentation/readmeExample.svg)\n\nBased on `AppNavigationTree`, the following navigation paths are valid:\n```\n/home\n/home/detail?id=0\n/home/settings\n```\n\nMore information on the `NavigationTree` and how to compose `PathBuilder`s can be found [here](https://github.com/Bahn-X/swift-composable-navigator/wiki/NavigationTree).\n\n## Vanilla SwiftUI + ComposableNavigator\nLet's go back to our vanilla SwiftUI home view and enhance it using the ComposableNavigator.\n\n```swift\nimport ComposableNavigator\n\nstruct HomeView: View {\n  @Environment(\\.navigator) var navigator\n  @Environment(\\.currentScreenID) var currentScreenID\n\n  var body: some View {\n    VStack {\n      Button(\n        action: goToDetail,\n        label: { Text(\"Show detail screen for 0\") }\n      )\n\n      Button(\n        action: goToSettings,\n        label: { Text(\"Go to settings screen\") }\n      )\n    }\n  }\n\n  func goToDetail() {\n    navigator.go(\n      to: DetailScreen(detailID: \"0\"),\n      on: currentScreenID\n    )\n  }\n\n  func goToSettings() {\n    navigator.go(\n      to: SettingsScreen(),\n      on: HomeScreen()\n    )\n  }\n}\n```\n\nWe can now inject the `Navigator` and `currentScreenID` in our tests and cover calls to goToDetail / goToSettings on an ExampleView instance in unit tests.\n\n## Integrating ComposableNavigator\n```swift\nimport ComposableNavigator\nimport SwiftUI\n\nstruct AppNavigationTree: NavigationTree {\n  let homeViewModel: HomeViewModel\n  let detailViewModel: DetailViewModel\n  let settingsViewModel: SettingsViewModel\n\n  var builder: some PathBuilder {\n    Screen(\n      HomeScreen.self,\n      content: {\n        HomeView(viewModel: homeViewModel)\n      },\n      nesting: {\n        DetailScreen.Builder(viewModel: detailViewModel)\n        SettingsScreen.Builder(viewModel: settingsViewModel)\n      }\n    )\n  }\n}\n\n@main\nstruct ExampleApp: App {\n  let dataSource = Navigator.Datasource(root: HomeScreen())\n\n  var body: some Scene {\n    WindowGroup {\n      Root(\n        dataSource: dataSource,\n        pathBuilder: AppNavigationTree(...)\n      )\n    }\n  }\n}\n```\n\n## Deeplinking\nAs **ComposableNavigator** builds the view hierarchy based on navigation paths, it is the ideal companion to implement deeplinking. Deeplinks come in different forms and shapes, however **ComposableNavigator** abstracts it into a first-class representation in the form of the `Deeplink` type. The **ComposableDeeplinking** library that is part of the **ComposableNavigator** contains a couple of helper types that allow easily replace the current navigation path with a new navigation path based on a `Deeplink` by defining a `DeeplinkHandler` and a composable `DeeplinkParser`.\n\nMore information on deeplinking and how to implement it in your own application can be found [here](https://github.com/Bahn-X/swift-composable-navigator/wiki/Deeplinking).\n\n## Dependency injection\n**ComposableNavigator** was inspired by [The Composable Architecture (TCA)](https://github.com/pointfreeco/swift-composable-architecture) and its approach to Reducer composition, dependency injection and state management. As all view building closures flow together in one central place, the app navigation tree, ComposableNavigator gives you full control over dependency injection. Currently, the helper package **ComposableNavigatorTCA** is part of this repository and the main package therefore has a dependency on TCA. This will change in the future when **ComposableNavigatorTCA** gets [extracted into its own repository](https://github.com/Bahn-X/swift-composable-navigator/issues/12).\n\n## Installation\n**ComposableNavigator** supports Swift Package Manager and contains two products, *ComposableNavigator* and *ComposableDeeplinking*.\n\n### Swift Package\nIf you want to add **ComposableNavigator** to your Swift packages, add it as a dependency to your `Package.swift`.\n\n```swift\ndependencies: [\n    .package(\n      name: \"ComposableNavigator\",\n      url: \"https://github.com/Bahn-X/swift-composable-navigator.git\",\n      from: \"0.1.0\"\n    )\n],\ntargets: [\n    .target(\n        name: \"MyAwesomePackage\",\n        dependencies: [\n            .product(name: \"ComposableNavigator\", package: \"ComposableNavigator\"),\n            .product(name: \"ComposableDeeplinking\", package: \"ComposableNavigator\")\n        ]\n    ),\n]\n```\n\n### Xcode\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Documentation/xc.png\" width=\"70%\"\u003e\u003c/img\u003e\u003c/p\u003e\n\nYou can add **ComposableNavigator** to your project via Xcode. Open your project, click on **File → Swift Packages → Add Package Dependency…**, enter the repository url (https://github.com/Bahn-X/swift-composable-navigator.git) and add the package products to your app target.\n\n## Example application\n\u003cp align=\"center\"\u003e\u003cimg src=\"./Documentation/exampleapp.gif\" width=\"40%\"\u003e\u003c/img\u003e\u003c/p\u003e\n\nThe **ComposableNavigator** repository contains [an example application](./Example) showcasing a wide range of library features and path builder patterns that are also applicable in your application. The example app is based on **ComposableNavigator** + **TCA** but also shows how to navigate via the navigator contained in a view's environment as you could do it in a Vanilla SwiftUI application.\n\nThe Example application contains a UI test suite that is run on every pull request. In that way, we can make sure that, even if SwiftUI changes under the hood, **ComposableNavigator** behaves as expected.\n\n## Documentation\nThe latest ComposableNavigator documentation is available in the [wiki](https://github.com/Bahn-X/swift-composable-navigator/wiki).\n\n## Contribution\nThe contribution process for this repository is described in [CONTRIBUTING](./CONTRIBUTING.md). We welcome contribution and look forward to your ideas.\n\n## License\nThis library is released under the MIT license. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBahn-X%2Fswift-composable-navigator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FBahn-X%2Fswift-composable-navigator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBahn-X%2Fswift-composable-navigator/lists"}