{"id":13778594,"url":"https://github.com/rebeloper/NavigationKit","last_synced_at":"2025-05-11T12:31:05.499Z","repository":{"id":169311306,"uuid":"321378387","full_name":"rebeloper/NavigationKit","owner":"rebeloper","description":"🧭 SwiftUI navigation done right","archived":false,"fork":false,"pushed_at":"2021-02-18T09:32:37.000Z","size":116,"stargazers_count":176,"open_issues_count":6,"forks_count":15,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-11-13T09:11:57.971Z","etag":null,"topics":["dismiss","navigation","navigationcontroller","navigationview","navigator","pop","presentview","presentviewcontroller","pushviewcontroller","sheet","swiftui","swiftui-components","swiftui-framework","swiftui-learning","swiftuikit"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rebeloper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-12-14T14:45:46.000Z","updated_at":"2024-11-01T08:45:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"42e2c908-a135-4423-8393-774dc454dbd9","html_url":"https://github.com/rebeloper/NavigationKit","commit_stats":null,"previous_names":["rebeloper/navigationkit"],"tags_count":78,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rebeloper%2FNavigationKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rebeloper%2FNavigationKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rebeloper%2FNavigationKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rebeloper%2FNavigationKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rebeloper","download_url":"https://codeload.github.com/rebeloper/NavigationKit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225048956,"owners_count":17412900,"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":["dismiss","navigation","navigationcontroller","navigationview","navigator","pop","presentview","presentviewcontroller","pushviewcontroller","sheet","swiftui","swiftui-components","swiftui-framework","swiftui-learning","swiftuikit"],"created_at":"2024-08-03T18:00:55.160Z","updated_at":"2024-11-17T14:30:38.546Z","avatar_url":"https://github.com/rebeloper.png","language":"Swift","readme":"# 🧭 NavigationKit\n\n![swift v5.3](https://img.shields.io/badge/swift-v5.3-orange.svg)\n![platform iOS](https://img.shields.io/badge/platform-iOS-blue.svg)\n![deployment target iOS 14](https://img.shields.io/badge/deployment%20target-iOS%2014-blueviolet)\n![YouTube tutorial](https://img.shields.io/badge/YouTube-video%20tutorial-red)\n\n**NavigationKit** is a lightweight library which makes `SwiftUI` navigation super easy to use.\n\n## 💻 Installation\n### 📦 Swift Package Manager\nUsing \u003ca href=\"https://swift.org/package-manager/\" rel=\"nofollow\"\u003eSwift Package Manager\u003c/a\u003e, add it as a Swift Package in Xcode 11.0 or later, `select File \u003e Swift Packages \u003e Add Package Dependency...` and add the repository URL:\n```\nhttps://github.com/rebeloper/NavigationKit.git\n```\n### ✊ Manual Installation\nDownload and include the `NavigationKit` folder and files in your codebase.\n\n### 📲 Requirements\n- iOS 14+\n- Swift 5.3+\n\n## 🎬 Video Tutorial\n\n\u003cp\u003e\u003ca href=\"https://www.youtube.com/watch?v=gk-sTNvOsR8\u0026list=PL_csAAO9PQ8Yj7ZU7n2IJjIsqcFaLcvJN\u0026index=4\"\u003eSwiftUI Navigation - How to Navigate in SwiftUI Apps on YouTube\u003c/a\u003e\u003c/p\u003e\n\nThis tutorial was made for `v.0.1.0.` I have improved and made `NavigationKit` even easier to use since this video. Read on to see how to use the newest version.\n\n## 👉 Import\n\nImport `NavigationKit` into your `View`\n\n```\nimport NavigationKit\n```\n\n## 🧳 Features\n\nHere's the list of the awesome features `NavigationKit` has:\n- [X] default / custom transitions\n- [X] push\n- [X] push to view with id\n- [X] pop\n- [X] pop to view with id\n- [X] pop to root\n- [X] present (as modal / full screen)\n- [X] may disable swipe down to dismiss on modal\n- [X] dismiss\n- [X] dismiss to root\n- [X] combine `push` with `present` (good for showing Login / Onboarding / Tutorial)\n- [X] built in Navigation Bars as view-modifiers (or build and use your own dream nav bar)\n- [X] works perfectly with `TabView`\n\nIn SwiftUI navigtion is handeled by the `NavigationView` and `NavigationLink`. At the moment these views have some limitations:\n\n- transition animations cannot be turned off or customised;\n- we can't navigate back either to root (i.e. the first app view), or to a specific view;\n- we can't push programmatically without using a `View`;\n- customizing the `NavigationBar` is limited or it has to be done via `UINavigationBar.appearance()` (using `UIKit` 😒);\n- `presenting` a view modally is done with the `.sheet` and `.fullScreenCover` view-modifiers adding confusion to the `NavigationLink`'s `push` like pattern; \n\n`NavigationKitView` is a view that mimics all the behaviours belonging to the standard `NavigationView`, but it adds the features listed here above. You have to wrap your view hierarchy inside a `NavigationKitView`:\n\n```\nimport NavigationKit\n\nstruct RootView: View {\n    var body: some View {\n        NavigationKitView {\n            Tab_0_0_View()\n        }\n    }\n}\n```\n\nYou can even customise transitions and animations in some different ways. The `NavigationKitView` will apply them to the hierarchy: \n\n- you could decide to go for no transition at all by creating the navigation stack this way `NavigationStackView(transitionType: .none)`;\n- you could create the navigation stack with a custom transition:\n\n```\nimport NavigationKit\n\nstruct RootView: View {\n    var body: some View {\n        NavigationKitView(transitionType: .custom(.scale)) {\n            Tab_0_0_View()\n        }\n    }\n}\n```\n`NavigationKitView` has a default easing for transitions. The easing can be customised during the initialisation\n```\nstruct RootView: View {\n    var body: some View {\n        NavigationKitView(transitionType: .custom(.scale), easing: .spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5)) {\n            Tab_0_0_View()\n        }\n    }\n}\n```\n**Important:** The above is the recommended way to customise the easing function for your transitions. Please, note that you could even specify the easing this other way:\n\n```\nNavigationKitView(transitionType: .custom(AnyTransition.scale.animation(.spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5))))\n```\n\nAttaching the easing directly to the transition? **Don't do this**. SwiftUI has still some problems with implicit animations attached to transitions, so it may not work. For example, implicit animations attached to a .slide transition won't work.\n\n## ⬅️ Push\n\nIn order to navigate forward you have to `push` with an optional `delay`:\n\n```\nimport NavigationKit\n\nstruct Tab_0_0_View: View {\n    \n    @EnvironmentObject private var navigation: Navigation\n    \n    var body: some View {\n        VStack {\n            Button {\n                navigation.push(Tab_0_1_View(), delay: 1.5)\n            } label: {\n                Text(\"Next\")\n            }\n            Spacer()\n        }\n    }\n}\n```\n\nMake sure you are using a view model in order for values to persist between push/pop operations. SwiftUI resets all the properties of a view marked with `@State` every time the view is removed from a view hierarchy. For the `NavigationKitView` this is a problem because when I come back to a previous view (with a pop operation) I want all my view controls to be as I left them before (for example I want my `TextField`s to contain the text I previously typed in). It seems that the solution to this problem is using the `.id` modifier specifying an id for the views I don't want SwiftUI to reset. According to the Apple documentation the `.id` modifier:\n\n\u003e Generates a uniquely identified view that can be inserted or removed.\n\nbut again, it seems that this API is currently not working as expected (take a look at this interesting post: https://swiftui-lab.com/swiftui-id/). In order to workaround this problem, then, you have to use `@ObservableObject` when you need to make some state persist between push/pop operations.\n\n```\nimport NavigationKit\n\nstruct Tab_0_0_View: View {\n    \n    @EnvironmentObject private var navigation: Navigation\n    @ObservedObject private var viewModel = Tab_0_0_ViewModel()\n    \n    var body: some View {\n        VStack {\n            \n            TextField(\"Type something...\", text: $viewModel.text)\n                .textFieldStyle(RoundedBorderTextFieldStyle())\n            \n            Button {\n                self.viewModel.fetchData { (result) in\n                    switch result {\n                    case .success(let finished):\n                        if finished {\n                            navigation.push(Tab_0_2_View())\n                        } else {\n                            print(\"Something went wrong\")\n                        }\n                    case .failure(let err):\n                        print(err.localizedDescription)\n                    }\n                }\n            } label: {\n                Text(\"Push after model operation\")\n            }\n            Spacer()\n        }\n    }\n}\n```\n\n## 🆔 Specifying an ID \n\nIt's not mandatory, but if you want to come back to a specific view at some point later you need to specify an ID for that view:\n\n```\nButton {\n    navigation.push(Tab_0_1_View(), withId: \"Tab_0_1_View\")\n} label: {\n    Text(\"Next\")\n}\n```\nYou will be able to `pop` to this view using the `id`. Read on. 🤓\n\n## ➡️ Pop\n\nPop operation works as the push operation, with an optional `delay`:\n\n```\nButton {\n    navigation.pop(delay: 1.5)\n} label: {\n    Label(\"Back\", systemImage: \"chevron.backward\")\n}\n```\nwhich pops to the previous view. You can even specify a destination for your pop operation:\n```\nButton {\n    navigation.pop(to: .view(withId: \"Tab_0_1_View\"))\n} label: {\n    Text(\"Pop to Tab_0_1_View\")\n}\n```\nWe can also pop to root like so:\n```\nButton {\n    navigation.pop(to: .root)\n} label: {\n    Text(\"Pop to Root\")\n}\n```\n\n## 🚧 NavigationBar\n\n`NavigationKit` replaces `NavigationView` altogether. In order to see a navigation bar you can create your own or use the built in view modifiers. You must add them as a modifier of a `VStack` which contains a `Spacer` to push its content up.\n\n### Inline navigation bar\n\n```\nVStack {\n    ...\n    Spacer()\n}\n.inlineNavigationBar(titleView:\n            Text(\"Tab_0_1_View\").bold(),\n        leadingView:\n            Button {\n                navigation.pop()\n            } label: {\n                Label(\"Back\", systemImage: \"chevron.backward\")\n            },\n        trailingView:\n            Button {\n                navigation.push(Tab_0_2_View())\n            } label: {\n                Text(\"Next\")\n            },\n        backgroundView:\n            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)\n)\n```\n\n### Large navigation bar\n\n```\nVStack {\n    ...\n    Spacer()\n}\n.largeNavigationBar(titleView:\n            Text(\"Tab_0_0_View\").bold().lineLimit(1),\n        leadingView:\n            EmptyView(),\n        trailingView:\n            Button {\n                navigation.push(Tab_0_1_View(), withId: \"Tab_0_1_View\")\n            } label: {\n                Text(\"Next\")\n            },\n        backgroundView:\n            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)\n)\n```\n\n### Custom navigation bar\n\n```\nvar body: some View {\n    VStack {\n        ...\n        Spacer()\n    }.customNavigationBar(titleView:\n                            HStack {\n                                Text(\"TODAY\").font(.title).fontWeight(.light)\n                                Spacer()\n                                Text(todayString().uppercased()).font(.title).fontWeight(.light)\n                            },\n                          backgroundView:\n                            Color(.secondarySystemBackground).edgesIgnoringSafeArea(.top)\n    )\n}\n\nfunc todayString() -\u003e String {\n    let formatter = DateFormatter()\n    formatter.dateFormat = \"EEE MM/dd\"\n    return formatter.string(from: Date())\n}\n```\n\n## ⬆️ Present\n\nPresenting a modal is a bit diferent than pushing:\n\n1. create a `@State` variable for your view;\n2. add a `Sheet` or `FullScreenSheet` view with an optional `onDismiss` callback. You must add it to the view hierarchy. Don't worry they are `EmptyView`s;\n3. activate the modal with `present()`\n\n**IMPORTANT NOTE**: you can present a `NavigationKitView` inside a `Sheet` / `FullScreenSheet` 😎\n\n```\nimport NavigationKit\n\nstruct Tab_1_0_View: View {\n    \n    // 1.\n    @State private var navigationForTab_0_0_View = false\n    @State private var navigationForTab_1_1_View = false\n    \n    @State private var navigationForTab_0_0_View_onDismiss = false\n    @State private var navigationForTab_1_1_View_onDismiss = false\n    \n    var body: some View {\n        VStack {\n            Button {\n                // 3.\n                navigationForTab_0_0_View_onDismiss.present()\n            } label: {\n                Text(\"Present with onDismiss callback\")\n            }\n            \n            Button {\n                // 3.\n                navigationForTab_1_1_View_onDismiss.present()\n            } label: {\n                Text(\"Present with onDismiss callback\")\n            }\n            \n            Spacer()\n            \n            // 2.\n            Sheet(isPresented: $navigationForTab_0_0_View) {\n                NavigationKitView {\n                    Tab_0_0_View() // \u003c- contains push navigation\n                }\n            }\n            \n            // 2.\n            FullScreenSheet(isPresented: $navigationForTab_1_1_View) {\n                NavigationKitView {\n                    Tab_1_1_View()\n                }\n            }\n            \n            // 2.\n            Sheet(isPresented: $navigationForTab_0_0_View_onDismiss) {\n                print(\"Dismissed Sheet. Do something here.\")\n            } content: {\n                NavigationKitView {\n                    Tab_0_0_View()\n                }\n            }\n            \n            // 2.\n            FullScreenSheet(isPresented: $navigationForTab_1_1_View_onDismiss) {\n                print(\"Dismissed FullScreenSheet. Do something here.\")\n            } content: {\n                NavigationKitView {\n                    Tab_1_1_View()\n                }\n            }\n        }\n        .padding()\n        .largeNavigationBar(titleView:\n                    Text(\"Tab_1_0_View\").bold().lineLimit(1),\n                leadingView:\n                    Button {\n                        // 3.\n                        navigationForTab_0_0_View.present()\n                    } label: {\n                        Text(\"Present Navigation\")\n                    },\n                trailingView:\n                    Button {\n                        // 3.\n                        navigationForTab_1_1_View.present()\n                    } label: {\n                        Text(\"Present\")\n                    },\n                backgroundView:\n                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)\n        )\n    }\n}\n```\n\n## ⬇️ Dismiss\n\nHere's how you can dismiss the modal:\n\n1. grab the `presentationMode` environment\n2. dimiss with it's `wrappedValue`\n\n```\nstruct Tab_1_1_View: View {\n    \n    // 1.\n    @Environment(\\.presentationMode) var presentationMode\n    \n    var body: some View {\n        VStack {\n            Color(.systemRed).edgesIgnoringSafeArea(.all)\n        }\n        .largeNavigationBar(titleView:\n                    Text(\"Tab_1_1_View\").bold().lineLimit(1),\n                leadingView:\n                    Button {\n                        // 2.\n                        presentationMode.wrappedValue.dismiss()\n                    } label: {\n                        Text(\"Dismiss\")\n                    },\n                trailingView:\n                    EmptyView(),\n                backgroundView:\n                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)\n        )\n    }\n}\n\n```\n\nYou may also disable swipe down on the `Sheet`:\n\n```\nSheet(isPresented: $navigationForTab_1_3_View) {\n    NavigationKitView {\n        Tab_1_3_View().disableSwipeToDismiss()\n    }\n}\n```\n\nIf you want to dismiss to root you want to use `@Binding`s and dismiss in order. `0.25` is the optimal delay:\n\n```\nstruct Tab_1_3_View: View {\n    @Environment(\\.presentationMode) var presentationMode\n    \n    @Binding var rootView: Bool\n    @Binding var secondRootView: Bool\n    @Binding var thirdRootView: Bool\n    \n    var body: some View {\n        VStack {\n            Color(.systemRed).edgesIgnoringSafeArea(.all)\n        }\n        .largeNavigationBar(titleView:\n                    Text(\"Tab_1_3_View\").bold().lineLimit(1),\n                leadingView:\n                    EmptyView(),\n                trailingView:\n                    Button {\n                        DispatchQueue.main.asyncAfter(deadline: .now()) {\n                            thirdRootView.dismiss()\n                        }\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {\n                            secondRootView.dismiss()\n                        }\n                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                            rootView.dismiss()\n                        }\n                    } label: {\n                        Text(\"Dismiss to Root\")\n                    },\n                backgroundView:\n                    Color(.tertiarySystemBackground).edgesIgnoringSafeArea(.top)\n        )\n    }\n}\n```\n\n## 🪁 Demo project\n\nFor a comprehensive Demo project check out: \n\u003ca href=\"https://github.com/rebeloper/NavigationKitDemo\"\u003eNavigationKitDemo\u003c/a\u003e\n\n## ✍️ Contact\n\n\u003ca href=\"https://rebeloper.com/\"\u003erebeloper.com\u003c/a\u003e / \n\u003ca href=\"https://www.youtube.com/rebeloper/\"\u003eYouTube\u003c/a\u003e / \n\u003ca href=\"https://store.rebeloper.com/\"\u003eShop\u003c/a\u003e / \n\u003ca href=\"https://rebeloper.com/mentoring\"\u003eMentoring\u003c/a\u003e\n\n## 📃 License\n\nThe MIT License (MIT)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n","funding_links":[],"categories":["Navigation"],"sub_categories":["Content"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frebeloper%2FNavigationKit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frebeloper%2FNavigationKit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frebeloper%2FNavigationKit/lists"}