Coordinators in SwiftUI. Simple, powerful and elegant.

Overview

Stinsen

Language Platform License

Simple, 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. The library is developed during working hours for the Byva app.

Why? πŸ€”

We 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.

What is a Coordinator? πŸ€·πŸ½β€β™‚οΈ

Normally in SwiftUI a 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.

How do I use Stinsen? πŸ§‘πŸΌβ€πŸ«

Example using a Navigation Stack:

class ProjectsCoordinator: NavigationCoordinatable {
    var navigationStack = NavigationStack() // usually you would want to initialize this without any active children

    enum Route {
        case project(id: UUID)
        case createProject
    }
    
    func resolveRoute(route: Route) -> Transition {
        switch route {
        case .project(let id):
            return .push(AnyView(ProjectSummaryScreen(id: id)))
        case .createProject:
            return .modal(AnyCoordinatable(CreateProjectCoordinator()))
        }
    }
    
    @ViewBuilder func start() -> some View {
        ProjectsScreen()
    }
}

The Route-enum defines all the possible routes that can be performed from the current coordinator. The function resolve(route: Route) is responsible for providing the transition and the actual view/coordinator that we will route to. This can be combined with a factory in the coordinator as well.

Using a router, which has a reference to the coordinator, we can perform transitions from a view - but also pop to the previous screen, dismissing the coordinator, switch tab etc (depending on the type of coordinator). Inside the view, the router can be fetched using @EnvironmentObject.

struct ProjectsScreen: View {
    @EnvironmentObject var projects: NavigationRouter<ProjectsCoordinator.Route>
    
    var body: some View {
        List {
          /* ... */
        }
        .navigationBarItems(
            trailing: Button(
                action: { projects.route(to: .createProject) },
                label: { Image(systemName: "doc.badge.plus") }
            )
        )
    }
}

You 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.

Stinsen out of the box has three different kinds of Coordinatable protocols your coordinators can implement:

  • NavigationCoordinatable - For navigational flows. Make sure to wrap these in a NavigationViewCoordinator somewhere if you wish to push on the navigation stack.
  • TabCoordinatable - For TabViews.
  • ViewCoordinatable - Just a view and routes that do not push but rather replace the entire view, can be used for instance when switching between logged in/logged out.

ViewModel Support πŸ–₯

Since @EnvironmentObject only can be accessed within a View, Stinsen provides two methods of passing the router to the ViewModel.

Via onAppear

struct ProjectsScreen: View {
    @EnvironmentObject var projects: NavigationRouter<ProjectsCoordinator.Route>
    
    var body: some View {
        List {
          /* ... */
        }
        .onAppear {
            viewModel.router = projects
        }
    }
}

RouterObject

The RouterStore saves the instance of the router and you can get it via a custom PropertyWrapper. This provides a nice decoupling between View and ViewModel.

To retrieve a router:

class LoginScreenViewModel: ObservableObject {
    
    // directly via the RouterStore
    var main: ViewRouter<MainCoordinator.Route>? = RouterStore.shared.retrieve()
    
    // via the RouterObject property wrapper
    @RouterObject
    var unauthenticated: NavigationRouter<UnauthenticatedCoordinator.Route>?
    
    init() {
        
    }
    
    func loginButtonPressed() {
        main?.route(to: .authenticated)
    }
    
    func forgotPasswordButtonPressed() {
        unauthenticated?.route(to: .forgotPassword)
    }
}

Installation πŸ’Ύ

Stinsen supports two ways of installation, Cocoapods and SPM.

SPM

Open 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

Cocoapods

Create a Podfile in your app's root directory. Add

# Podfile
use_frameworks!

target 'YOUR_TARGET_NAME' do
    pod 'Stinsen'
end

Sample App πŸ“±

Stinsen Sample App

Clone the repo and run the StinsenApp 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.

Who are responsible? πŸ™‹πŸ½β€β™€οΈ

At 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.

Why the name "Stinsen"? πŸš‚

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.

License πŸ“ƒ

Stinsen is released under an MIT license. See LICENCE for more information.

Comments
  • No children, cannot dismiss?!

    No children, cannot dismiss?!

    Hello, in my app I have very simple coordinator, inited from SceneDelegate. When I try to dismiss modal window "CitySelectView" by router?.dismiss() I got "fatalError("no children, cannot dismiss?!")"

    What have I done wrong?

    final class WallsListCoordinator: NavigationCoordinatable {
        var navigationStack: NavigationStack = NavigationStack()
        
        enum Route: NavigationRoute {
            case openCalculator(id: Int?)
            case openCatalogue
            case openCitySelect
        }
        
        func resolveRoute(route: Route) -> Transition {
            switch route {
            case .openCalculator(let id):
                return .push(
                    AnyView(
                        Resolver.resolve(CalculatorFormView.self, args: id)
                    )
                )
            case .openCatalogue:
                return .push(
                    AnyCoordinatable(
                        CatalogueCoordinator()
                    )
                )
                
            case .openCitySelect:
                return .modal(
                    AnyView(
                        NavigationView {
                            Resolver.resolve(CitySelectView.self)
                        }
                    )
                )
            }
        }
    
    opened by Soloqub 9
  • How to implement Deep-link

    How to implement Deep-link

    First of all, thank you for creating a great library.

    I'm using this library to implement Deep-link.

    Deep-link need to be able to work on any screen, so we'll need a coordinator that can be used for any screen. Is this possible?

    I'd appreciate it if you could let me know if there's any other way besides the one I said.

    Thanks.

    opened by PangMo5 7
  • PopToRoot shows second view while transitioning from third to first view

    PopToRoot shows second view while transitioning from third to first view

    I have a coordinator and three screens:

    @ViewBuilder
    func makeStart() -> some View {
        TimetableView()
    }
    
    @ViewBuilder
    func makeJoinMeeting() -> some View {
        JoinMeetingView()
    }
    
    @ViewBuilder
    func makeMeeting() -> some View {
        MeetingView()
    }
    

    While on the third view I'm trying to .popToRoot in order to transition to the timetable. And sometimes the second view shows for a moment during the transition. Did anybody encounter this?

    opened by veerlorden 6
  • Use pop() inside NavigationCoordinatable

    Use pop() inside NavigationCoordinatable

    Hello again! I wold like to pop back inside of coordinator. Is there any way to implement something like this?

    func makeView() -> some View {
      viewModel.$switchSearchType
          .sink { [weak self] _ in
              self?.pop() // no such method
          }
          .store(in: &cancellables)
    ...
        return View()
    }
    
    opened by ivedeneev 6
  • How to create a custom TabCoordinatable

    How to create a custom TabCoordinatable

    Is there any way to create a custom TabCoordinatable? What I want is basically the function of menu changing, but I don't want the coordinator's view to be a tab view, but a custom menu instead.

    I had a look at the source and tried to create my own protocol, but I'm getting several error messages stating that the children.dismissalAction is unaccessible due to internal protection level.

    Any way to implement this?

    opened by jblanco-applaudostudios 6
  • add RouterStore and RouterObject PropertyWrapper

    add RouterStore and RouterObject PropertyWrapper

    This is a follow up to https://github.com/rundfunk47/stinsen/issues/5

    As I described in the issue I use Resolver for injecting my dependencies. Since using it directly within Resolver is messy I created a RouterStore which kind of copies the SwiftUI Environment behavior. It allows to use the Router without directly passing them from via the View, so there is no tight coupling.

    I created a new protocol called RouterIdentifiable which has to be implemented optionally by a Coordinator. This way the code is 100% backward compatibly.

    It would be cool if you like my solution our if we could find a similar solution enabling better ViewModel usage :blush:

    opened by savage7 6
  • EmptyView while push

    EmptyView while push

    Hi! When I use push, I have empty view with back Botton on toolbar. But when I use .modal or .fullScreen - everything is ok

    final class AccountCoordinator: NavigationCoordinatable {
        let stack = Stinsen.NavigationStack<AccountCoordinator>(initial: \AccountCoordinator.start)
        @Root var start = getAccount
        @Route(.push) var userProfile = getUserProfile
    
        @ViewBuilder func getAccount() -> some View {
            AccountView()
        }
    
        @ViewBuilder func getUserProfile() -> some View {
            UserProfileView()
        }
    }
    
    final class TabCoordinator: TabCoordinatable {
        lazy var child = TabChild(startingItems: [
            \TabCoordinator.dashBoards,
            \TabCoordinator.results,
            \TabCoordinator.bookings,
            \TabCoordinator.accounts
        ], activeTab: 0)
    
        @Route(tabItem: dashBoardTab) var dashBoards = getLocations
        @Route(tabItem: bookingTab) var bookings = getBooking
        @Route(tabItem: accountTab) var accounts = getAccount
        @Route(tabItem: resultsTab) var results = getResults
    
        let dashBoard: DashCoordinator
        let booking: BookingCoordinator
        let account: AccountCoordinator
        let result: ResultsCoordinator
    
    
    image
    opened by dmikots 5
  • Does stinsen support installation with CocoaPods?

    Does stinsen support installation with CocoaPods?

    First of all, thank you for providing such a simple and elegant way to implement coordinator pattern with SwiftUI, I have been sniffing around about this topic and your approach is the best I found.

    I installed this library with Swift Package Manager very easily and had zero issues when using it. But my project also depends on some other third party libraries that do not support SPM, so I also had to use CocoaPods to install those. And now, for some Apple M1 chip and arm64 compilation reason, I couldn't make the project compile for both CocoaPods and SPM dependencies, so I am thinking about dropping SPM and use CocoaPods only, because I know how to make it work with CocoaPods.

    So here we are back to my original question: do you currently or plan to support installation with CocoaPods? I tried searching stinsen on CocoaPods.org but could not find it. Also, if you can add a Installation section to your readme, it would definitely help some devs to use your framework more easily.

    Thanks again, for sharing this great library!

    opened by hdmdhr 5
  • [question] TabBar Button tap gesture

    [question] TabBar Button tap gesture

    Hello, thank you for providing a good coordinator pattern library.

    We tried to make the TabBar Button move to the corresponding tabView root when pressed, and move to the top of the scrollView when clicked again, but we found that tapGesture does not work for the TabBar Button provided by stinsen.

    @ViewBuilder func makeFirstTab(isActive: Bool) -> some View {
        VStack {
            Image("icon")
            Text("TabItem")
        }
        .onTapGesture {
            // Not working
        }
    }
    

    I'm looking for a way other than how to create a Custom TabBar to save resources and I was wondering if there is a better way inside the library?

    Thanks

    opened by okstring 4
  • How to pass parameters to a make func

    How to pass parameters to a make func

    I have the following scenario: i want to pass parameters to a function that returns a view:

    @ViewBuilder
        private func makeMailComposeVC(recepients: [String], capturedImage: UIImage, completion: @escaping Action) -> some View {
            MailComposeViewController(toRecipients: recepients, capturedImage: capturedImage, didFinish: completion)
        }
    

    but here it says: "No exact matches in call to initalizer"

    @Route(.modal) var mailCompose = makeMailComposeVC

    opened by razvanrujoiu 4
  • Remove previous stacked Views with SwiftUI

    Remove previous stacked Views with SwiftUI

    I am using Stinsen on SwiftUI to navigate from one view to another.

    does exist way to remove previous stacked view?

    View A -> View B -> ViewC -> View D go back ---> View B (ViewC should be removed from the stack so on back i should go to viewA again.

    NavigationStack has value, but 'value' is inaccessible due to 'internal' protection level.

    opened by yury-streamscloud 4
  • hasRoot / isRoot crashes app

    hasRoot / isRoot crashes app

    Hi! I've adapted example app to handle deeplink, but app crashes when trying to get current coordinator:

    final class AppCoordinator: NavigationCoordinatable {
        var stack: Stinsen.NavigationStack<AppCoordinator>
    
        @Root var unauthenticated = makeUnauthenticated
        @Root var authenticated = makeAuthenticated
    
        init() {
            let user = User() // obtaining user from persistent store
            stack = NavigationStack(initial: \AppCoordinator.authenticated, user) // this is also handled by auth state listeners
        }
        
         func makeUnauthenticated() -> NavigationViewCoordinator<UnauthenticatedCoordinator> {
            NavigationViewCoordinator(UnauthenticatedCoordinator())
        }
    
        func makeAuthenticated(param user: User) -> NavigationViewCoordinator<MainCoordinator> {
            NavigationViewCoordinator(MainCoordinator(user: user)) // MainCoordinator is a simple navigaion view coordinator
        }
    
        func handleDynamicLink(_ url: URL) {
            guard let coordinator = self.hasRoot(\.authenticated) else { // <-- Crashes here.  self.isRoot(\.authenticated) causes the same crash
                return
            }
        }
    }
    

    Crash in NavigationCoordinatable.swift, line 661, col 21

    Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

    Printing description of route: <ReferenceWritableKeyPath<AppCoordinator, Transition<AppCoordinator, RootSwitch, User, NavigationViewCoordinator>>: 0x280687280> Printing description of inputItem: nil

    ((input: App.User, comparator: ())?) inputItem = nil Printing description of self: <AppCoordinator: 0x283d59500>

    Screenshot 2022-09-18 at 13 27 54

    App and coordination logic works perfectly, but this check on AppCoordinator level causes the crash.

    It looks that unwraping var root: NavigationRoot! from NavigationStack (NavigationStack.swift, line 29) causes the crash. What is the reason for this crash and how to solve it?

    Stinsen (2.0.7) MacOS 12.6, XCode 14 Release, iOS 15-16

    opened by emartinson 0
  • Crash while unwrapping coordinator in NavigationRouter

    Crash while unwrapping coordinator in NavigationRouter

    Hello,

    I encountered an AppCrash in the "coordinator" getter of NavigationRouter due to the force cast on the type.

    When bringing back the App from background I sometimes get: #6 (null) in swift_dynamicCast ()

    I am calling it this way (after unwrapping the router safely): _ = overviewRouter.coordinator.customPopToRoot()

    Stacktrace:

    0 (null) in __pthread_kill () 6 (null) in swift_dynamicCast () 7 0x000000010325a530 in NavigationRouter.coordinator.getter () 8 0x0000000102f8d720 in specialized NavService.goToOverviewRoot() 9 0x000000010370ab20 in LifecycleService.reactOnStatusChange() 10 0x000000010370a92c in LifecycleService.updateTo(state:) 11 (null) in protocol witness for LifecycleServiceType.updateTo(state:) in conformance LifecycleService () 12 0x00000001030183dc in App.handleAppState(_:) 13 (null) in partial apply for closure 2 in closure 1 in App.body.getter ()

    Most likely because of the force cast of the weakReference in NavigationRouter.coordinator. Did you ever encounter this problem or have an idea how to solve it (other than making coordinator optional)?

    Best regards Steffen

    opened by Steffenberg 2
  • focusFirst doesn't move to previous page

    focusFirst doesn't move to previous page

    using stinsen 2.0.9 and try to move to previous page from current stack with focusFirst. Debug shows that stack has been changed internally but displayed page doesn't changed. By the way method popToRoot works well - returns to the begin of the stack.

    opened by ivan-kolesov 0
  • TabBar: present modal view from tabbar item

    TabBar: present modal view from tabbar item

    Hi everyone,

    First of all, thank you for the great framework.

    I have a question. Is it possible to present the modal view from tabbar item? If yes, how can I do it? In the app I'm working on, I need to force users to log in before they can reach a specific tabbar item.

    opened by Luur 2
  • StateObject causes crashes with Stinsen

    StateObject causes crashes with Stinsen

    We have implemented StateObject into an app, as shown in the example below and we receive a lot of issues with the Coordinator such as crashes. The problems start to arise as soon as the StateObject is changed (due to updates). During debugging I have found out that the RouterStore holds a lot of Coordinators twice or three times and I guess this is the reason for the issues. However, I was not able to yet to create a minimal example for easier debugging of this issue.

    struct TestApp: App {
        @StateObject var manager = Manager()
       
        var body: some Scene {
            WindowGroup {
                NavigationViewCoordinator(
                    MainCoordinator()
                )
                .view()
                .environmentObject(manager)
            }
        }
    }
    
    opened by Kondamon 5
Owner
Narek Mailian
Hi! I'm rundfunk47. Currently CTO at @Byva. Creating solutions for myself and others πŸ‘·πŸ½β€β™‚οΈ
Narek Mailian
Powerful navigation in the Composable Architecture via the coordinator pattern

TCACoordinators The coordinator pattern in the Composable Architecture TCACoordinators brings a flexible approach to navigation in SwiftUI using the C

John Patrick Morgan 231 Jan 7, 2023
Simple example for the coordinator design pattern and using th Xcoordinator pod

Cordinator-Pattern-Sample This an Example and base for the coordinator design pattern using the XCoordinator pod ?? XCoordinator is a navigation frame

Ali Fayed 3 Sep 13, 2022
Simple and integrated way to customize navigation bar experience on iOS app.

NavKit Simple and integrated way to customize navigation bar experience on iOS app. It should save our time that we usually use to make abstraction of

Wilbert Liu 37 Dec 7, 2022
Simple custom navigation bar by swift

YoNavBarView Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements Installation YoNav

null 1 Nov 23, 2021
FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator

FlowStacks allow you to manage complex SwiftUI navigation and presentation flows with a single piece of state. This makes it easy to hoist that state into a high-level coordinator view. Using this pattern, you can write isolated views that have zero knowledge of their context within the navigation flow of an app.

John Patrick Morgan 471 Jan 3, 2023
An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Composable Navigator An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind Vanilla S

Bahn-X 539 Jan 8, 2023
An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation.

swiftui-navigation-stack An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations

Matteo 753 Jan 2, 2023
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

SwiftUI Navigation Tools for making SwiftUI navigation simpler, more ergonomic and more precise. Motivation Tools Navigation overloads Navigation view

Point-Free 1.1k Jan 1, 2023
SwiftUINavigator: a lightweight, flexible, and super easy library which makes SwiftUI navigation a trivial task

The logo is contributed with ❀️ by Mahmoud Hussein SwiftUINavigator is a lightwe

OpenBytes 22 Dec 21, 2022
Customizable multi platform menu bar component with the dark and light scheme support - SwiftUI

Menu bar component (SwiftUI) Features Observing menu selection changes via generic PreferenceKey The color intensity automatically adjusts depending o

Igor 3 Aug 13, 2022
custom navigationBar in swiftui

SwiftUI-WRNavigationBar custom navigationBar in swiftui Requirements iOS 14.0 Installation pod 'SwiftUI-WRNavigationBar', '~>1.1.1' Overview debug cus

wangrui460 29 Nov 7, 2022
sRouting - The lightweight navigation framework for SwiftUI.

sRouting The lightweight navigation framework for SwiftUI. Overview sRouting using the native navigation mechanism in SwiftUI. It's easy to handle nav

Shiro 8 Aug 15, 2022
Change SwiftUI Navigation Bar Color for different View

SwiftUINavigationBarColor Change SwiftUI NavigationBar background color per screen. Usage For NavigationBarColor to work, you have to set the Navigati

Hai Feng Kao 18 Jul 15, 2022
Path based routing in SwiftUI

Easy and maintainable app navigation with path based routing for SwiftUI. With SwiftUI Router you can power your SwiftUI app with path based routing.

Freek 652 Dec 28, 2022
🧭 SwiftUI navigation done right

?? NavigationKit NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. ?? Installation ?? Swift Package Manager Usi

Alex Nagy 152 Dec 27, 2022
Router is a library that assists with SwiftUI view transitions.

Router Router is a library that assists with SwiftUI view transitions. Installation .package(name: "Router", url: "[email protected]:1amageek/Router.git"

1amageek 69 Dec 23, 2022
NavigationViewKit is a NavigationView extension library for SwiftUI.

NavigationViewKit δΈ­ζ–‡η‰ˆθ―΄ζ˜Ž NavigationViewKit is a NavigationView extension library for SwiftUI. For more detailed documentation and demo, please visit 用N

δΈœε‘θ‚˜ε­ 74 Dec 28, 2022
Navigation helpers for SwiftUI applications build with ComposableArchitecture

Swift Composable Presentation ?? Description Navigation helpers for SwiftUI applications build with ComposableArchitecture. More info about the concep

Dariusz Rybicki 52 Dec 14, 2022
Make SwiftUI Navigation be easy

VNavigator VNavigator is a clean and easy-to-use navigation in SwiftUI base on UINavigationController in UIKit Installation From CocoaPods CocoaPods i

Vu Vuong 10 Dec 6, 2022