{"id":13462708,"url":"https://github.com/rundfunk47/stinsen","last_synced_at":"2025-05-16T06:07:53.249Z","repository":{"id":37070392,"uuid":"340845988","full_name":"rundfunk47/stinsen","owner":"rundfunk47","description":"Coordinators in SwiftUI. Simple, powerful and elegant.","archived":false,"fork":false,"pushed_at":"2024-07-26T09:29:19.000Z","size":1876,"stargazers_count":939,"open_issues_count":43,"forks_count":105,"subscribers_count":19,"default_branch":"master","last_synced_at":"2025-05-09T10:25:13.882Z","etag":null,"topics":["coordinator","coordinator-pattern","coordinators","ios","macos","macosx","mvvm","mvvm-c","swift","swift-library","swift5","swiftui","tvos","watchos"],"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/rundfunk47.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-21T07:43:58.000Z","updated_at":"2025-05-02T03:38:47.000Z","dependencies_parsed_at":"2024-01-17T00:55:11.255Z","dependency_job_id":"47a80fef-481b-45d1-b5fd-9588e8086584","html_url":"https://github.com/rundfunk47/stinsen","commit_stats":{"total_commits":154,"total_committers":18,"mean_commits":8.555555555555555,"dds":0.2142857142857143,"last_synced_commit":"d6ad23f4c68212fed8ac64c739bef224628776e3"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rundfunk47%2Fstinsen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rundfunk47%2Fstinsen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rundfunk47%2Fstinsen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rundfunk47%2Fstinsen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rundfunk47","download_url":"https://codeload.github.com/rundfunk47/stinsen/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254478193,"owners_count":22077676,"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":["coordinator","coordinator-pattern","coordinators","ios","macos","macosx","mvvm","mvvm-c","swift","swift-library","swift5","swiftui","tvos","watchos"],"created_at":"2024-07-31T13:00:19.245Z","updated_at":"2025-05-16T06:07:48.239Z","avatar_url":"https://github.com/rundfunk47.png","language":"Swift","funding_links":[],"categories":["Uncategorized","Navigation","Swift","HarmonyOS","Swift UI"],"sub_categories":["Uncategorized","Content","Windows Manager"],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./Images/wordmark.svg\" alt=\"Stinsen\"\u003e\n\u003c/p\u003e\n\n[![Language](https://img.shields.io/static/v1.svg?label=language\u0026message=Swift%205\u0026color=FA7343\u0026logo=swift\u0026style=flat-square)](https://swift.org)\n[![Platform](https://img.shields.io/static/v1.svg?label=platforms\u0026message=iOS%20|%20tvOS%20|%20watchOS%20|%20macOS\u0026logo=apple\u0026style=flat-square)](https://apple.com)\n[![License](https://img.shields.io/cocoapods/l/Crossroad.svg?style=flat-square)](https://github.com/rundfunk47/stinsen/blob/main/LICENSE)\n\nSimple, powerful and elegant implementation of the Coordinator pattern in SwiftUI. Stinsen is written using 100% SwiftUI which makes it work seamlessly across iOS, tvOS, watchOS and macOS devices.\n\n# Why? 🤔\n\nWe all know routing in UIKit can be hard to do elegantly when working with applications of a larger size or when attempting to apply an architectural pattern such as MVVM. Unfortunately, SwiftUI out of the box suffers from many of the same problems as UIKit does: concepts such as `NavigationLink` live in the view-layer, we still have no clear concept of flows and routes, and so on. _Stinsen_ was created to alleviate these pains, and is an implementation of the _Coordinator Pattern_. Being written in SwiftUI, it is completely cross-platform and uses the native tools such as `@EnvironmentObject`. The goal is to make _Stinsen_ feel like a missing tool in SwiftUI, conforming to its coding style and general principles.\n\n# What is a Coordinator? 🤷🏽‍♂️ \n\nNormally in SwiftUI, the view has to handle adding other views to the navigation stack using `NavigationLink`. What we have here is a tight coupling between the views, since the view must know in advance all the other views that it can navigate between. Also, the view is in violation of the _single-responsibility principle_ (SRP). Using the Coordinator Pattern, presented to the iOS community by Soroush Khanlou at the NSSpain conference in 2015, we can delegate this responsibility to a higher class: The Coordinator.\n\n# How do I use Stinsen? 👩🏼‍🏫 \n\n## Defining the coordinator\nExample using a Navigation Stack:\n\n```swift\nfinal class UnauthenticatedCoordinator: NavigationCoordinatable {\n    let stack = NavigationStack(initial: \\UnauthenticatedCoordinator.start)\n    \n    @Root var start = makeStart\n    @Route(.modal) var forgotPassword = makeForgotPassword\n    @Route(.push) var registration = makeRegistration\n    \n    func makeRegistration() -\u003e RegistrationCoordinator {\n        return RegistrationCoordinator()\n    }\n    \n    @ViewBuilder func makeForgotPassword() -\u003e some View {\n        ForgotPasswordScreen()\n    }\n    \n    @ViewBuilder func makeStart() -\u003e some View {\n        LoginScreen()\n    }\n}\n\n```\n\nThe `@Route`s defines all the possible routes that can be performed from the current coordinator and the transition that will be performed. The value on the right hand side is the factory function that will be executed when routing. The function can return either a SwiftUI view or another coordinator. The `@Root` another type of route that has no transition, and used for defining the first view of the coordinator's navigation stack, which is referenced by the `NavigationStack`-class.  \n\n_Stinsen_ out of the box has two different kinds of `Coordinatable` protocols your coordinators can implement: \n\n* `NavigationCoordinatable` - For navigational flows. Make sure to wrap these in a NavigationViewCoordinator if you wish to push on the navigation stack.\n* `TabCoordinatable` - For TabViews.\n\nIn addition, _Stinsen_ also has two Coordinators you can use, `ViewWrapperCoordinator` and `NavigationViewCoordinator`. `ViewWrapperCoordinator` is a coordinator you can either subclass or use right away to wrap your coordinator in a view, and `NavigationViewCoordinator` is a `ViewWrapperCoordinator` subclass that wraps your coordinator in a `NavigationView`.   \n\n## Showing the coordinator for the user\nThe view for the coordinator can be created using `.view()`, so in order to show a coordinator to the user you would just do something like:\n\n```swift\nstruct StinsenApp: App {\n    var body: some Scene {\n        WindowGroup {\n            MainCoordinator().view()\n        }\n    }\n}\n```\n\n_Stinsen_ can be used to power your whole app, or just parts of your app. You can still use the usual SwiftUI `NavigationLink`s and present modal sheets inside views managed by _Stinsen_, if you wish to do so.\n\n## Navigating from the coordinator\nUsing a router, which has a reference to both the coordinator and the view, we can perform transitions from a view. Inside the view, the router can be fetched using `@EnvironmentObject`. Using the router one can transition to other routes:\n\n```swift\nstruct TodosScreen: View {\n    @EnvironmentObject var todosRouter: TodosCoordinator.Router\n    \n    var body: some View {\n        List {\n          /* ... */\n        }\n        .navigationBarItems(\n            trailing: Button(\n                action: {\n                    // Transition to the screen to create a todo:\n                    todosRouter.route(to: \\.createTodo) \n                },\n                label: { \n                    Image(systemName: \"doc.badge.plus\") \n                }\n            )\n        )\n    }\n}\n```\n\nYou can also fetch routers referencing coordinators that appeared earlier in the tree. For instance, you may want to switch the tab from a view that is inside the `TabView`.\n\nRouting can be performed directly on the coordinator itself, which can be useful if you want your coordinator to have some logic, or if you pass the coordinator around:\n\n```swift\nfinal class MainCoordinator: NavigationCoordinatable {\n    @Root var unauthenticated = makeUnauthenticated\n    @Root var authenticated = makeAuthenticated\n    \n    /* ... */\n    \n    init() {\n        /* ... */\n\n        cancellable = AuthenticationService.shared.status.sink { [weak self] status in\n            switch status {\n            case .authenticated(let user):\n                self?.root(\\.authentiated, user)\n            case .unauthenticated:\n                self?.root(\\.unauthentiated)\n            }\n        }\n    }\n}\n```\n\nWhat actions you can perform from the router/coordinator depends on the kind of coordinator used. For instance, using a `NavigationCoordinatable`, some of the functions you can perform are:\n\n* `popLast` - Removes the last item from the stack. Note that _Stinsen_ doesn't care if the view was presented modally or pushed, the same function is used for both. \n* `pop` - Removes the view from the stack. This function can only be performed by a router, since only the router knows about which view you're trying to pop.\n* `popToRoot` - Clears the stack.\n* `root` - Changes the root (i.e. the first view of the stack). If the root is already the active root, will do nothing.\n* `route` - Navigates to another route.\n* `focusFirst` - Finds the specified route if it exists in the stack, starting from the first item. If found, will remove everything after that.\n* `dismissCoordinator` - Deletes the whole coordinator and it's associated children from the tree.\n\n# Examples 📱\n\n\u003cimg src=\"./Images/stinsenapp-ios.gif\" alt=\"Stinsen Sample App\"\u003e\n\nClone the repo and run the _StinsenApp_ in _Examples/App_ to get a feel for how _Stinsen_ can be used. _StinsenApp_ works on iOS, tvOS, watchOS and macOS. It attempts to showcase many of the features _Stinsen_ has available for you to use. Most of the code from this readme comes from the sample app. There is also an example showing how _Stinsen_ can be used to apply a testable MVVM-C architecture in SwiftUI, which is available in _Example/MVVM_.\n\n# Advanced usage 👩🏾‍🔬\n\n## ViewModel Support\n\nSince `@EnvironmentObject` only can be accessed within a `View`, _Stinsen_ provides a couple of ways of routing from the ViewModel. You can inject the coordinator through the ìnitializer, or register it at creation and resolve it in the viewmodel through a dependency injection framework. These are the recommended ways of doing this, since you will have maximum control and functionality. \n\nOther ways are passing the router using the `onAppear` function:\n\n```swift\nstruct TodosScreen: View {\n    @StateObject var viewModel = TodosViewModel() \n    @EnvironmentObject var projects: TodosCoordinator.Router\n    \n    var body: some View {\n        List {\n          /* ... */\n        }\n        .onAppear {\n            viewModel.router = projects\n        }\n    }\n}\n```\n\nYou can also use what is called the `RouterStore` to retreive the router. The `RouterStore` saves the instance of the router and you can get it via a custom PropertyWrapper.\n\nTo retrieve a router:\n```swift\nclass LoginScreenViewModel: ObservableObject {\n    \n    // directly via the RouterStore\n    var main: MainCoordinator.Router? = RouterStore.shared.retrieve()\n    \n    // via the RouterObject property wrapper\n    @RouterObject\n    var unauthenticated: Unauthenticated.Router?\n    \n    init() {\n        \n    }\n    \n    func loginButtonPressed() {\n        main?.root(\\.authenticated)\n    }\n    \n    func forgotPasswordButtonPressed() {\n        unauthenticated?.route(to: \\.forgotPassword)\n    }\n}\n```\n\nTo see this example in action, please check the MVVM-app in _Examples/MVVM_.\n\n## Customizing\n\nSometimes you'd want to customize the view generated by your coordinator. NavigationCoordinatable and TabCoordinatable have a `customize`-function you can implement in order to do so: \n\n```swift\nfinal class AuthenticatedCoordinator: TabCoordinatable {\n    /* ... */\n    @ViewBuilder func customize(_ view: AnyView) -\u003e some View {\n        view\n            .onReceive(Services.shared.$authentication) { authentication in\n                switch authentication {\n                case .authenticated:\n                    self.root(\\.authenticated)\n                case .unauthenticated:\n                    self.root(\\.unauthenticated)\n                }\n            }\n        }\n    }\n}\n```\n\nThere is also a `ViewWrapperCoordinator` you can use to customize as well.\n\n## Chaining\n\nSince most functions on the coordinator/router return a coordinator, you can use the results and chain them together to perform more advanced routing, if needed. For instance, to create a SwiftUI buttons that will change the tab and select a specific todo from anywhere in the app after login:\n\n```swift\nVStack {\n    ForEach(todosStore.favorites) { todo in\n        Button(todo.name) {\n            authenticatedRouter\n                .focusFirst(\\.todos)\n                .child\n                .popToRoot()\n                .route(to: \\.todo, todo.id)\n        }\n    }\n}\n```\n\nThe `AuthenticatedCoordinator` referenced by the `authenticatedRouter` is a `TabCoordinatable`, so the function will:\n\n* `focusFirst`: return the first tab represented by the route `todos` and make it the active tab, unless it already is the active one.\n* `child`: will return it's child, the `Todos`-tab is a `NavigationViewCoordinator` and the child is the `NavigationCoordinatable`.\n* `popToRoot`: will pop away any children that may or may not have been present.\n* `route`: will route to the route `Todo` with the specified id. \n\nSince Stinsen uses KeyPaths to represent the routes, the functions are type-safe and invalid chains cannot be created. This means: if you have a route in _A_ to _B_ and in _B_ to _C_, the app will not compile if you try to route from _A_ to _C_ without routing to _B_ first. Also, you cannot perform actions such as `popToRoot()` on a `TabCoordinatable` and so on.\n\n## Deep Linking\n\nUsing the returned values, you can easily deeplink within the app:\n\n```swift\nfinal class MainCoordinator: NavigationCoordinatable {\n    @ViewBuilder func customize(_ view: AnyView) -\u003e some View {\n        view.onOpenURL { url in\n            if let coordinator = self.hasRoot(\\.authenticated) {\n                do {\n                    // Create a DeepLink-enum\n                    let deepLink = try DeepLink(url: url, todosStore: coordinator.todosStore)\n                    \n                    switch deepLink {\n                    case .todo(let id):\n                        coordinator\n                            .focusFirst(\\.todos)\n                            .child\n                            .route(to: \\.todo, id)\n                    }\n                } catch {\n                    print(error.localizedDescription)\n                }\n            }\n        }\n    }\n}\n```\n\n## Creating your own Coordinatable\n\n_Stinsen_  comes with a couple of _Coordinatables_ for standard SwiftUI views. If you for instance want to use it for a Hamburger-menu, you need to create your own. Check the source-code to get some inspiration.\n\n# Installation 💾\n\n_Stinsen_ supports two ways of installation, Cocoapods and SPM. \n\n## SPM\n\nOpen Xcode and your project, click `File / Swift Packages / Add package dependency...` .  In the textfield \"_Enter package repository URL_\", write `https://github.com/rundfunk47/stinsen` and press _Next_ twice\n\n## Cocoapods\n\nCreate a `Podfile` in your app's root directory. Add\n```\n# Podfile\nuse_frameworks!\n\ntarget 'YOUR_TARGET_NAME' do\n    pod 'Stinsen'\nend\n```\n# Known issues and bugs 🐛\n\n* _Stinsen_ does not support `DoubleColumnNavigationViewStyle`. The reason for this is that it does not work as expected due to issues with `isActive` in SwiftUI. _Workaround:_ Use UIViewRepresentable or create your own implementation.\n* _Stinsen_ works pretty bad in various older versions of iOS 13 due to, well, iOS 13 not really being that good at SwiftUI. Rather than trying to set a minimum version that _Stinsen_ supports, you're on your own if you're supporting iOS 13 to figure out whether or not the features you use actually work. Generally, version 13.4 and above seem to work alright.\n\n# Who are responsible? 🙋🏿‍♂️\n\nAt Byva we strive to create a 100% SwiftUI application, so it is natural that we needed to create a coordinator framework that satisfied this and other needs we have. The framework is used in production and manages ~50 flows and ~100 screens. The framework is maintained by [@rundfunk47](https://github.com/rundfunk47/).\n\n# Why the name \"Stinsen\"? 🚂 \n\n_Stins_ is short in Swedish for \"Station Master\", and _Stinsen_ is the definite article, \"The Station Master\". Colloquially the term was mostly used to refer to the Train Dispatcher, who is responsible for routing the trains. The logo is based on a wooden statue of a _stins_ that is located near the train station in Linköping, Sweden.\n\n# Updating from Stinsen v1 🚀\n\nThe biggest change in Stinsen v2 is that it is more type-safe than Stinsen v1, which allows for easier chaining and deep-linking, among other things.\n\n* The Route-enum has been replaced with property wrappers. \n* `AnyCoordinatable` has been replaced with a protocol. It does not perform the same duties as the old `AnyCoordinatable` and does not fit in with the more type-safe routing of version 2, so remove it from your project.\n* Enums are not used for routes, now _Stinsen_ uses keypaths. So instead of `route(to: .a)` we use `route(to: \\.a)`.\n* CoordinatorView has been removed, use `.view()`.\n* Routers are specialized using the coordinator instead of the route.\n* Minor changes to functions and variable names.\n* Coordinators need to be marked as final.\n* ViewCoordinatable has been removed and folded into NavigationCoordinatable. Use multiple `@Root`s and switch between them using `.root()` to get the same functionality.\n\n# License 📃\n\n_Stinsen_ is released under an MIT license. See LICENCE for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frundfunk47%2Fstinsen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frundfunk47%2Fstinsen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frundfunk47%2Fstinsen/lists"}