sti

Stinsen

Coordinators in SwiftUI. Simple, powerful and elegant.

Showing:

Popularity

Downloads/wk

0

GitHub Stars

128

Maintenance

Last Commit

11d ago

Contributors

4

Package

Dependencies

0

License

MIT License

Categories

Readme

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.

Known issues and bugs 🐛

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

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.

Rate & Review

Great Documentation0
Easy to Use0
Performant0
Highly Customizable0
Bleeding Edge0
Responsive Maintainers0
Poor Documentation0
Hard to Use0
Slow0
Buggy0
Abandoned0
Unwelcoming Community0
100