Giter VIP home page Giter VIP logo

stinsen's Introduction

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.

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

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

Defining the coordinator

Example using a Navigation Stack:

final class UnauthenticatedCoordinator: NavigationCoordinatable {
    let stack = NavigationStack(initial: \UnauthenticatedCoordinator.start)
    
    @Root var start = makeStart
    @Route(.modal) var forgotPassword = makeForgotPassword
    @Route(.push) var registration = makeRegistration
    
    func makeRegistration() -> RegistrationCoordinator {
        return RegistrationCoordinator()
    }
    
    @ViewBuilder func makeForgotPassword() -> some View {
        ForgotPasswordScreen()
    }
    
    @ViewBuilder func makeStart() -> some View {
        LoginScreen()
    }
}

The @Routes 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.

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

  • NavigationCoordinatable - For navigational flows. Make sure to wrap these in a NavigationViewCoordinator if you wish to push on the navigation stack.
  • TabCoordinatable - For TabViews.

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

Showing the coordinator for the user

The 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:

struct StinsenApp: App {
    var body: some Scene {
        WindowGroup {
            MainCoordinator().view()
        }
    }
}

Stinsen can be used to power your whole app, or just parts of your app. You can still use the usual SwiftUI NavigationLinks and present modal sheets inside views managed by Stinsen, if you wish to do so.

Navigating from the coordinator

Using 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:

struct TodosScreen: View {
    @EnvironmentObject var todosRouter: TodosCoordinator.Router
    
    var body: some View {
        List {
          /* ... */
        }
        .navigationBarItems(
            trailing: Button(
                action: {
                    // Transition to the screen to create a todo:
                    todosRouter.route(to: \.createTodo) 
                },
                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.

Routing 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:

final class MainCoordinator: NavigationCoordinatable {
    @Root var unauthenticated = makeUnauthenticated
    @Root var authenticated = makeAuthenticated
    
    /* ... */
    
    init() {
        /* ... */

        cancellable = AuthenticationService.shared.status.sink { [weak self] status in
            switch status {
            case .authenticated(let user):
                self?.root(\.authentiated, user)
            case .unauthenticated:
                self?.root(\.unauthentiated)
            }
        }
    }
}

What 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:

  • 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.
  • 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.
  • popToRoot - Clears the stack.
  • root - Changes the root (i.e. the first view of the stack). If the root is already the active root, will do nothing.
  • route - Navigates to another route.
  • focusFirst - Finds the specified route if it exists in the stack, starting from the first item. If found, will remove everything after that.
  • dismissCoordinator - Deletes the whole coordinator and it's associated children from the tree.

Examples πŸ“±

Stinsen Sample App

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

Advanced usage πŸ‘©πŸΎβ€πŸ”¬

ViewModel Support

Since @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.

Other ways are passing the router using the onAppear function:

struct TodosScreen: View {
    @StateObject var viewModel = TodosViewModel() 
    @EnvironmentObject var projects: TodosCoordinator.Router
    
    var body: some View {
        List {
          /* ... */
        }
        .onAppear {
            viewModel.router = projects
        }
    }
}

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

To retrieve a router:

class LoginScreenViewModel: ObservableObject {
    
    // directly via the RouterStore
    var main: MainCoordinator.Router? = RouterStore.shared.retrieve()
    
    // via the RouterObject property wrapper
    @RouterObject
    var unauthenticated: Unauthenticated.Router?
    
    init() {
        
    }
    
    func loginButtonPressed() {
        main?.root(\.authenticated)
    }
    
    func forgotPasswordButtonPressed() {
        unauthenticated?.route(to: \.forgotPassword)
    }
}

To see this example in action, please check the MVVM-app in Examples/MVVM.

Customizing

Sometimes 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:

final class AuthenticatedCoordinator: TabCoordinatable {
    /* ... */
    @ViewBuilder func customize(_ view: AnyView) -> some View {
        view
            .onReceive(Services.shared.$authentication) { authentication in
                switch authentication {
                case .authenticated:
                    self.root(\.authenticated)
                case .unauthenticated:
                    self.root(\.unauthenticated)
                }
            }
        }
    }
}

There is also a ViewWrapperCoordinator you can use to customize as well.

Chaining

Since 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:

VStack {
    ForEach(todosStore.favorites) { todo in
        Button(todo.name) {
            authenticatedRouter
                .focusFirst(\.todos)
                .child
                .popToRoot()
                .route(to: \.todo, todo.id)
        }
    }
}

The AuthenticatedCoordinator referenced by the authenticatedRouter is a TabCoordinatable, so the function will:

  • focusFirst: return the first tab represented by the route todos and make it the active tab, unless it already is the active one.
  • child: will return it's child, the Todos-tab is a NavigationViewCoordinator and the child is the NavigationCoordinatable.
  • popToRoot: will pop away any children that may or may not have been present.
  • route: will route to the route Todo with the specified id.

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

Deep Linking

Using the returned values, you can easily deeplink within the app:

final class MainCoordinator: NavigationCoordinatable {
    @ViewBuilder func customize(_ view: AnyView) -> some View {
        view.onOpenURL { url in
            if let coordinator = self.hasRoot(\.authenticated) {
                do {
                    // Create a DeepLink-enum
                    let deepLink = try DeepLink(url: url, todosStore: coordinator.todosStore)
                    
                    switch deepLink {
                    case .todo(let id):
                        coordinator
                            .focusFirst(\.todos)
                            .child
                            .route(to: \.todo, id)
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }
        }
    }
}

Creating your own Coordinatable

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.

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

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.

Updating from Stinsen v1 πŸš€

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

  • The Route-enum has been replaced with property wrappers.
  • 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.
  • Enums are not used for routes, now Stinsen uses keypaths. So instead of route(to: .a) we use route(to: \.a).
  • CoordinatorView has been removed, use .view().
  • Routers are specialized using the coordinator instead of the route.
  • Minor changes to functions and variable names.
  • Coordinators need to be marked as final.
  • ViewCoordinatable has been removed and folded into NavigationCoordinatable. Use multiple @Roots and switch between them using .root() to get the same functionality.

License πŸ“ƒ

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

stinsen's People

Contributors

abrown252 avatar dscyrescotti avatar ianclawson avatar lepips avatar mattjung avatar mhero21 avatar nullic avatar rundfunk47 avatar savage7 avatar simonmoser-bluesource avatar skorulis avatar syaifulq avatar thomato avatar vascoorey avatar veronikaklikarova avatar zalazara avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stinsen's Issues

Make a `RouterStore` per `NavigationCoordinatable`

I have a use case that I have a 3 tabs, they each holds a NavigationCoordinatable. Each tab can navigates to some screens, and there are shared screens between tabs.

I made 1 NavigationCoordinatable type for both 3 tabs as the screen routes are pretty much the same.

Now within each tab, if I reference the coordiantor.route using @EnvironmentObject, everything's fine. However, when use RouterStore, since it's shared one, the router object I get is incorrect (becasue I have 3 same ones).

I suggest RouterStore should propage the same as a @EnvironmentObject

Memory Leaks/Cycles

I think that there are some memory leaks in Stinsen (tested with 2.0.2). They are relatively easy to reproduce.

  • Open the testbed
  • Login
  • Logout
  • Login

or

  • Open the testbed
  • login
  • open todos
  • press add
  • close
  • press add
  • close

Outcome:
The coordinators seam to be strongly referenced and are not freed.

image

There seams to be a cycle problem:

image

It tried to find the root cause, like strong references in closures, but didn't find it 😿 .

Referencing instance method 'pop' on 'NavigationRouter' requires that 'NavigationCoordinatable' conform to 'Stinsen.NavigationCoordinatable

class ProductDetailsViewModel<NavigationCoordinatable>: ProductDetailsViewModelProtocol {
    
    @RouterObject var router: NavigationRouter<NavigationCoordinatable>!

    func pop() {
        router.pop()
    }
    
    func dismissCoordinator() {
        router.dismissCoordinator()
    }
}

I'm trying to pass the protocol as a generic into my ViewModel, in order to inject multiple coordinators that conform to NavigationCoordinatable but i'm getting this error:
Referencing instance method 'pop' on 'NavigationRouter' requires that 'NavigationCoordinatable' conform to 'Stinsen.NavigationCoordinatable'

[Question] What is the best way to deep link to the second/third screen in the app?

Suppose, I have a detail screen for every todo screen. In my app, the detail screen is called from the todo screen. Now, if I want to deep link to the todoDetail, can this be achieved by chaining as well?

I want to achieve something like this:

final class MainCoordinator: NavigationCoordinatable {
    @ViewBuilder func customize(_ view: AnyView) -> some View {
        view.onOpenURL { url in
            if let coordinator = self.hasRoot(\.authenticated) {
                do {
                    // Create a DeepLink-enum
                    let deepLink = try DeepLink(url: url, todosStore: coordinator.todosStore)
                    
                    switch deepLink {
                    case .todo(let id):
                        coordinator
                            .focusFirst(\.todos)
                            .child
                            .route(to: \.todo, id)
                            .route(to: \.todoDetail, id)            //     <------
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }
        }
    }
}

I guess, alternatives to that could be:

  • pass an argument to the todo screen and do the second push from there
  • do another switch deepLink on the todo screen in order to do the second push
    but both do not seem ideal to me. What would be the best way to achieve this?

Thanks for any help (and for this great library!)

Stinsen + IQKeyboardManager Navigation Bar issue

Hi 😊

I have a chat like screen within a NavigationCoordinatable with a TextField on the bottom. When I press the TextField the screen should move up. We use IQKeyboardManager for general TextField handling.

With Stinsen the NavigationBar is moved outside the Screen:
Simulator Screen Shot - iPhone 12 - 2021-09-02 at 07 32 13

If should look like this:
Simulator Screen Shot - iPhone 12 - 2021-09-02 at 07 32 00

I think I already found the root cause:
NavigationViewCoordinatorView sets StackNavigationViewStyle. When I remove this,
it looks as expected.

It tried to set the style directly to DefaultNavigationViewStyle inside the View but thats overruled by the NavigationViewCoordinatorView style.

Is there a proper workaround for this? (I've found none)
Why is StackNavigationViewStyle used explicitly? Can we make that somehow configurable or is StackNavigationViewStyle needed?

I modified the example app to show the problem. I've modified the LoginScreen and added IQKeyboardManager.
Stinsen + IQKeyboardManager Issue.zip

Thanks ALOT for your help and ongoing work in this great project πŸ₯‡ πŸ˜„

[Question] Is it possible to customize the NavigationCoordinatableView?

I have a requirement to use Stinsen as the navigation to open a custom sheet. While currently it support to route to the view as a modal that will using sheet under the hood, we actually want to use our own implementation of the sheet. Is it possible to create a custom NavigationCoordinatableView for now?

I'd tried to create one but it seems that the PresentationHelper and some properties of the NavigationStack are private/internal.

Pushing to stack when inside another navigation stack

Hello, I'm currently having an issue when having two nested NavigationViewCoordinatable.
Currently I have Coordinator1 that starts at its root, then I have a route that routes to Coordinator2, which is also a NavigationViewCoordinatable.

The problem is, if I don't define Coordinator2 as NavigationViewCoordinator<Coordinator2>, I cannot push to the stack, even though it is already inside its parent stack (Coordinator1). If I do define it as NavigationViewCoordinator<Coordinator2> I get duplicated Navigation Bars.

Is this behavior expected, how can I overcome this issue?

Create `rebuildRoot( ... )`

First off, amazing project that I plan to use in almost every SwiftUI project of mine.

In an application I'm working on, I have an instance where the app can be "reset" via deleting all of core data, etc.

At the bottom of my stack is view A with a view model that reads from CoreData upon appearing, I'm not using notifications or other state systems to detect core data changes for good, consistent practice. I have a modal popup B that appears from A that can do the "reset" action and dismisses itself, back to A.

Currently, root is described as: "If the root is already the active root, will do nothing." This is great, however I think a function called rebuildRoot( ... ) (name not final) would be helpful if I want to rebuild the root even if it's the same, active root. Or in other words, force a rebuild with the given root no matter what it is.

For me specifically, this would work well for clean view/view model practices. For now, I'll just have a workaround.

Dismiss coordinator and pop to root

Thank you for the wonderful framework
Can I kill the current coordinator and return to the first screen of the previous coordinator?
So far, that's all I've come up with:

@EnvironmentObject private var mainRouter: MainCoordinator.Router
@EnvironmentObject private var registrationCardRouter: RegistrationCardCoordinator.Router

var body: some View {
    Text("KopilkaScreen")
        .navigationBarBackButtonHidden(true)
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Button(action: back) {
                    Text("Back")
                }
            }
        }
}

func back() {
    registrationCardRouter.dismissCoordinator {
        mainRouter.popToRoot()
    }
}

But it shows first the last screen of the stack and a moment later the first screen.
Sorry for my English.

Late Init Issue on RouterObject

In the testbed code the ViewModel of the LoginScreen is created within the View itself:
@StateObject var viewModel = LoginScreenViewModel()

When the ViewModel is created i.e. in the Coordinator like this (before the Coordinator is created):
LoginScreen(viewModel: loginViewModel)

Then the RouterObject fails to retrieve the Router because it's only done once in the constructor of the RouterObject:
self.retreived = storage.retrieve()
The RouterObject fails to retrieve the Coordinator and navigation doesn't work.

Pushing from parent in nested NavigationCoordinatables

Hi there!
I recently discovered Stinsen and really like it - thanks! However, I'm struggling to implement a specific flow.

Let's say that I have a parent NavigationCoordinatable, SetupCoordinatable. It provides many sequential routes, akin to the common "wizard" flow:

  • a profile setup view,
  • a password setup view,
  • a couple of setup views about the user's "interests":
    • view A,
    • view B,
    • view C,
  • a final, "congrats"-type view.

So nothing out of the ordinary for a lot of mobile apps.

Now, I'd like to make this last set of views reusable, so I move it to its own NavigationCoordinatable, InterestsSetupCoordinatable. How can I push the last, "congrats" view, which belongs to the parent NavigationCoordinatable, without having to dismiss the whole child NavigationCoordinatable? Just calling .route on the parent unfortunately doesn't push anything.
I guess that's because the NavigationStack which is displayed is the child's, not the parent, but I'm not sure how to go to implement this flow.

Any ideas?

Pushing a View doesn't work (iOS 15 Preview)

Hello, I'm using Stinsen lib for navigating through app flows. Modal presenting works as expected, but pushing a view doesn't work. I'm using xcode 13.4 & tried 13.5 beta, but still viewcontroller doesn't get pushed. Following the examples from this repo.

Is it possible that ios 15 is not supported?

Custom Coordinatables on v2.0.0

Just wondering if you had an example of the hamburger menu using the new version, I can't seem to find it in the source.
I'm having trouble understanding how to write a custom router using the new system.

Getting random crashes - memory leaks?

I have implemented a simple app with Stinson using TabCoordinator. Unfortunately, I receive random crashes from time to time that are not reproducable. The crashes occur on real devices after using the app for multiple minutes and clicking on multiple views and navigationButtons. Sometimes a button A or another button B or C causes the crashes. The crashes can not be isolated nor reproduced on the simulator.

Please find a related crash log:

Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x0000000102d6f9c0
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: SIGNAL 5 Trace/BPT trap: 5
Terminating Process: exc handler [5126]

Triggered by Thread: 0

Thread 0 name:
Thread 0 Crashed:
0 Bore 0x0000000102d6f9c0 Swift runtime failure: Unexpectedly found nil while unwrapping an Optional value + 0 (NavigationCoordinatable.swift:0)
1 Bore 0x0000000102d6f9c0 NavigationCoordinatable.dismissChild(coordinator:action:) + 240 (NavigationCoordinatable.swift:308)
2 Bore 0x0000000102d85858 specialized ViewWrapperCoordinator.dismissChild(coordinator:action:) + 180 (ViewWrapperCoordinator.swift:7)
3 Bore 0x0000000102d6fecc NavigationCoordinatable.dismissCoordinator(:) + 136 (NavigationCoordinatable.swift:314)
4 Bore 0x0000000102d7aabc NavigationRouter
.dismissCoordinator(:) + 124 (NavigationRouter.swift:35)
5 Bore 0x00000001029ecf1c closure #1 in closure #1 in ChooseImagesSUI.makeUIViewController(context:) + 120 (ChooseImagesSUI.swift:22)
6 Bore 0x00000001029eb550 partial apply for closure #1 in AddBoreServices.saveBoreCreateVisionElements(:callback:) + 28 (AddBoreServices.swift:51)
7 Bore 0x00000001029ebb70 closure #1 in BoreImageSaver.store(
:completion:) + 160 (BoreImageSaver.swift:54)
8 Bore 0x0000000102a2fad8 thunk for @escaping @callee_guaranteed () -> () + 28 (:0)
9 libdispatch.dylib 0x00000001801ad924 _dispatch_call_block_and_release + 32 (init.c:1517)
10 libdispatch.dylib 0x00000001801af670 _dispatch_client_callout + 20 (object.m:560)
11 libdispatch.dylib 0x00000001801bdb70 dispatch_main_queue_callback_4CF + 944 (inline_internal.h:2601)
12 CoreFoundation 0x00000001804f5d84 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE + 16 (CFRunLoop.c:1795)
13 CoreFoundation 0x00000001804aff5c __CFRunLoopRun + 2540 (CFRunLoop.c:3144)
14 CoreFoundation 0x00000001804c3468 CFRunLoopRunSpecific + 600 (CFRunLoop.c:3268)
15 GraphicsServices 0x000000019c06738c GSEventRunModal + 164 (GSEvent.c:2200)
16 UIKitCore 0x0000000182e665d0 -[UIApplication run] + 1100 (UIApplication.m:3493)
17 UIKitCore 0x0000000182be4f74 UIApplicationMain + 364 (UIApplication.m:5047)
18 SwiftUI 0x0000000188182314 closure #1 in KitRendererCommon(
:) + 164 (UIKitApp.swift:35)
19 SwiftUI 0x00000001880b1508 runApp
(:) + 252 (:0)
20 SwiftUI 0x0000000188092cb8 static App.main() + 128 (App.swift:114)
21 Bore 0x00000001029ccaf4 main + 24 (MainApp.swift:14)
22 Bore 0x00000001029ccaf4 $main + 24 (:11)
23 Bore 0x00000001029ccaf4 main + 36 (TabbarCoordinator.swift:0)
24 dyld 0x0000000103045aa4 start + 520 (dyldMain.cpp:879)

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)
                    }
                )
            )
        }
    }

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!

Crash when presenting TabCoordinator as modal on top of NavigationCoordinator

I am trying to present a TabCoordinator as a modal on top of a NavigationCoordinator. This causes a crash, as calling route(to: from the NavigationCoordinatable to present the modal tries to set the parent on the coordinator it is presenting.

In the Stinsen code, TabCoordinator's parent has a fatalError on get/set.

Is it possible to present a TabCoordinator as a modal in a different manner? Or how could I achieve this?

Thanks.

Full screen presented view is being dismissed when a system alert appears.

Reproduction steps:

  • present a view in a NavigationCoordinatable coordinator using @Route(.fullScreen) var playback = playbackView
  • the presented view asks for microphone permission that triggers presenting a system alert
  • in the NavigationCoordinatable class the internal func appear(_ int: Int) is getting called by this alert presentation with id -1.
  • This causes a pop to root mechanism so the presented screen is being dismissed and the whole routing is broken from that point

Requesting the permission later did not help, it triggers the same mechanism.
Using a dedicated Coordinator for the presented screen did not help either.

I can add more code snippets if it helps.

Passing global stateful object around coordinators and views

I have a root coordinator and multiple child coordinators, and i am trying to pass and @EnvironmentObject around to the child coordinators to pass it further to the child views but i’m getting this warning: β€œAccessing StateObject’s object without being installed on a View. This will create a new instance each time”. How can i pass a global stateful object around whitout getting this?

NavigationStack and ViewChild should be initializable with route

we should be able to do:

class Coordinator: NavigationCoordinator {
    let navigationStack: NavigationStack

    enum Route: NavigationRoute {
        case c
    }

    init(withC: Bool) {
         if withC {
            navigationStack = NavigationStack([.c])
        } else {
            navigationStack = NavigationStack()
        }
    }
[...]

[Question] How can I pass parameters to another coordinator/viewModel

Hello. At first, thanks for the great library!

I have a question but can't find the answer. Is there a way what with I can pass one or a few parameters to my "module"?

Notes: I use the architecture which in common looks like MVVM. There are a few minor differences. But at first glance, it shouldn't affect the general way of implementing.

So. I have an MVVM module that is used frequently through my app. For example, let it be the module helps a user to select a country. But sometimes I need to configure this module with parameters that are received from another one. Is there any way for it?

Below you can see the part of code for example.

final class UnauthenticatedCoordinator: BaseNavigationCoordinator {
    let stack = NavigationStack(initial: \UnauthenticatedCoordinator.start)

    @Root var start = makeStart
    @ViewBuilder func makeStart() -> some View {
        OnBoardingBuilder(router: self, di: di).view
    }

    @Route(.push) var recovery = makeRecovery
    @ViewBuilder func makeRecovery() -> some View {
        // In this place I wanna add some parameters
        SelectCountryBuilder(di: di).view()
    }
}

It seems a frequent task to me and I'm a little bit confused that I couldn't figure out it.

`popToRoot(:)` action block not called when there is nothing to pop

Currently, the action block from func popToRoot(_ action: (() -> ())? = nil) in NavigationRouter is only called if there is something to pop in the navigation stack.
Would it be possible to change that behavior and perform the action block even if there's nothing to pop ?

Dismiss NavigationView

Hi! Im presenting coordinator like this

@Route(.modal) var card = makeCard
    
func makeCard() -> NavigationViewCoordinator<CardCoordinator> {
    NavigationViewCoordinator(CardCoordinator())
}

Card coordinator contains 3 screens in navigation stack.
Then im trying to dismiss it like this in last view in navigation stack:

router.dismissCoordinator {
      print("dismiss")
}

After that im facing crash in dismissChild method of NavigationCoordinatable

How can i dismiss it correctly?

Hiding Home Indicator Issues

An app that I am contributing to has a video player that must have the home indicator hidden. It was working before but implementing Stinsen has broken it. It is done in SwiftUI and we use this somewhat popular workaround.

I have provided the sample project: https://github.com/LePips/StinsenHideBar2

I ask that the project is checked out. This project has a view A that presents a view B. B should have the home indicator hidden. I am currently attempting to hide the home indicator on every view that I possibly can, even through customize(). I currently find it weird that only view A has the home indicator hidden.

If I could get some help that would be great but I understand issues can't be addressed ad hoc. Or, if I should be doing something else with Stinsen, guidance would be helpful.

Thanks!

TabCoordinatableView `child` should be a StateObject

When running my app using TabCoordnatable I noticed that when either pulling down control/Notification Center or getting a popup to confirm a purchase. That the tabs reset. My thoughts are that child property should be a StateObject when it is current an ObservedObject.

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()
}

[Question] Is there a way to disable animation when popToRoot ?

Hi, this's a great project ! 🀩
I love to use it. but I got an issue at the moment which might need some help.

For example, my navigation looks like
A -> ( B1 -> B2 ) -> C

Once, user from B2 -> C, I need dismiss the whole B coordinator reset the navigation stack to A -> C .
Currently, I'm using this code to make it work.

routerA?
.popToRoot()
.route(to: \.c)

But visually, the navigation got pop back animation from B2 -> C instead of standard push forward animation.
Is there a way like UIKit that we can disable the animation when popToRoot ?

func popToRootViewController(animated: Bool)

NavigationBarButtons are only occasionally working

The framework is pretty nice, however NavigationBarButtons are only occasionally working. When I use the navigation bar buttons multiple times they early stop working.

Reproducer:

  1. Replace in Example the TestbedEnvironmentObjectScreenwith this code
  2. Use a real device with e.g. iOS14.8 (Can not reproduce with iOS15)
  3. Click on Modal coordinator and "X" multiple times in a row
  4. The more often you click, the less responsive the "X" is. It could take 5-10 clicks until it react again.
struct TestbedEnvironmentObjectScreen: View {
    @EnvironmentObject var testbed: TestbedEnvironmentObjectCoordinator.Router
    @State var text: String = ""
    
    var body: some View {
        ScrollView {
            VStack {
                
                RoundedButton("Modal coordinator") {
                    testbed.route(to: \.modalCoordinator)
                }
                RoundedButton("Dismiss me!") {
                    testbed.dismissCoordinator {
                        print("bye!")
                    }
                }
            }
        }
        .navigationBarItems(trailing:
                                Button(action: {
            debugPrint("Touched close button.")
            testbed.dismissCoordinator()
        }, label: { Image(systemName: "xmark")
               
        }))
    }
}
``

[Question] ViewModel Handling

First off thanks for this library!! It seams to elegantly solve one of the most annoying SwiftUI issues 😊

Reading this documentation it found this:
"This @EnvironmentObject can be put into a ViewModel if you wish to follow the MVVM-C Architectural Pattern."

How do you actually do this? Since a ViewModel isn't a View it has not access to EnvironmentObjects and the resolution would fail.

Do you pass the coordinator to the ViewModel via constructor or property. Or did you find a nicer solution for this?

Using a Custom TabBar

In my project, I need to calculate the tab height. However, there is no way to access the existing TabBar in your library. If you can add the height value as an environment variable, would be nice. On the other hand, I am thinking that if you can add an ability to use Custom Tab Bar would be awesome.

What do you think? I'd like to hear your opinions

Animation Issue on sheet dismiss

I have a very strange issue when dismissing a modal sheet:
https://user-images.githubusercontent.com/971353/138907699-95854f11-2d9d-440e-8cfc-d1099fec3833.mp4

I created an very reduced example to reproduce this issue:
AnimationBug.zip

It's caused by two things:

  • AppCoordinator has a "start" route which is just white
  • After some time, in the real app there is a some user interaction going on, I switch to "onboarding" (AppCoordinator:22)
  • Then I press the Settings button
  • Then I press the Dismiss button
    Result:
    The sheet dismiss animation is somehow incomplete.

I can work around the problem by wrapping the OnboardingCoordinator into a NavigationViewCoordinator (see AppCoordinator:36). However this means that the controller has a NavigationView, which is not needed for this use case.

Do use Stinsen incorrectly or do you think that this is a SwiftUi Bug? Your help is very appreciated!!!

TabView Functionality

Hey,

Firstly - thank you so much for the OS love, it's brilliant.

Secondly - I'm using a TabCoordinatable to power a TabView, but by doing so in SwiftUI you lose some default functionality like popping to the root on active tab tap, and scrolling to top on tab tap. As far as I'm aware there's no SwiftUI workaround, but there's a package that seems to do the trick:

https://github.com/NicholasBellucci/StatefulTabView

Cheers!

Hidden navigation bar shows up after showing modal in detail screen

Hello, I found an interesting behavior in my project, I've spending a lot of time figuring out the root cause but I'm still unable to, posting here to determine if this is a bug or not.

  • I have a TabCoordinatable inside of a NavigationCoordinatable
  • I set the navigationBarHidden = true and set navigation title properly (for the back button title)
  • I then push to a detail view, push to one layer deeper
  • Then show a modal
  • Manually navigate back to the TabView
  • Issue: The originally hidden navigation bar appeared, after switching tabs it hides again

Here is a video of the issue that demonstrate the issue:

Screen.Recording.2022-03-06.at.1.35.52.PM.mov

This is an example project that will repro the issue in the video:
stinsen-example.zip

[Question] @Root animation

UIKit based Coordinators offered the ability to apply transition effects when replacing a root view i.e. with RxFlow you could just do this:

Flows.use(onboardingFlow, when: .ready) { [unowned self] flowRoot in
            UIView.transition(with: self.rootWindow, duration: 0.1, options: .transitionCrossDissolve, animations: {}, completion: nil)
            self.rootWindow.rootViewController = flowRoot
}

or with XCoordinator you can do:
.presentFullScreen(TabCoordinator().strongRouter, animation: .fade)

Both are used to replace root views, not for a modal transition.

It would be cool if there would be basic support for this in Stinsen like:
@Root(.fade) var start = makeStart

Would that be possible? I honestly didn't find a location in the code where a concrete view switch is happening which could be animated.

Present a view on top of a NavigationView

Hi,
I need to present a full-screen cover view. It must be half-transparent and with iOS 13 support, so I can't use fullScreen transition.

So I need to get NavigationView and wrap it into ZStack. There is the problem.
In Stinsen NavigationView created deep inside the lib, in MyNavigationViewCoordinatorView and I can't get it.

I solved it by rewriting MyNavigationViewCoordinatorView and removing the creation of NavigationView in it. Then I create NavigationView manually and wrap it in ContainerView.

Something with that:

class MainCoordinator: ViewCoordinatable {
    var children = ViewChild()
        
    enum Route: ViewRoute {
        case wallList
    }

    func resolveRoute(route: Route) -> AnyCoordinatable {
        switch route {
        case .wallList:
            let coord = WallsListCoordinator()
            return AnyCoordinatable(
                MyNavigationViewCoordinator(coord, customize: { view in
                    ContainerView {
                        view
                    }
                })
            )
        }
    }
    
    @ViewBuilder func start() -> some View {
        Resolver.resolve(StartView.self)
    }
}

struct ContainerView<Content: View>: View {
    @ViewBuilder var content: Content
    
    @StateObject var popup = Popup()
    
    var body: some View {
        ZStack {
            NavigationView {
                content
            }
            .environmentObject(popup)
            
            if self.popup.show {
                Color.black.opacity(0.8)
                    .edgesIgnoringSafeArea(.all)
            }
        }
    }
}

It works, but it's not a very good approach really. Is there any better way to do it?

I need to inform NavigationViewCoordinator that It shouldn't create NavigationView because I created and send it in customize variable.

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.

[Question] What is the best way to deep link to the second/third screen in the app?

Suppose, I have a detail screen for every todo screen. In my app, the detail screen is called from the todo screen. Now, if I want to deep link to the todoDetail, can this be achieved by chaining as well?

I want to achieve something like this:

final class MainCoordinator: NavigationCoordinatable {
    @ViewBuilder func customize(_ view: AnyView) -> some View {
        view.onOpenURL { url in
            if let coordinator = self.hasRoot(\.authenticated) {
                do {
                    // Create a DeepLink-enum
                    let deepLink = try DeepLink(url: url, todosStore: coordinator.todosStore)
                    
                    switch deepLink {
                    case .todo(let id):
                        coordinator
                            .focusFirst(\.todos)
                            .child
                            .route(to: \.todo, id)
                            .route(to: \.todoDetail, id)            //     <------
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }
        }
    }
}

I guess, alternatives to that could be:

  • pass an argument to the todo screen and do the second push from there
  • do another switch deepLink on the todo screen in order to do the second push
    but both do not seem ideal to me. What would be the best way to achieve this?

Thanks for any help (and for this great library!)

Unwanted popping/re-pushing when on navigation coordinator

Hello, I'm having an issue with the navigation stacks. I have a SidebarCoordinator that has a SplitviewCoordinator inside of it (both are custom coordinators I made). The SplitviewCoordinator shows two NavigationCoordinators side by side.

I'm having a problem that whenever I show or hide the sidebar, if any of the navigation stacks is not on the root it will pop and then re-push the current view on said stack. Is this a known issue? Or perhaps a mistake I made while coding the coordinator view?

Stinsen v2 suggestions

Following the conversation on #28

I think that SplitView is a really common use case, I would love to see it built into Stinsen by default! πŸ₯³ πŸ˜„ I have been playing with the new version and I really like the new routing system and ways to route. These are the suggestions that I've come up with when using v2:

  • Coordinators that conform to NavigationViewCoordinatable, should already be of type NavigationViewCoordinator<SomeCoordinator>

I came up with a workaround for this, maybe it is useful for Stinsen itself:

extension NavigationCoordinatable {
    func eraseToNavigationCoordinator() -> NavigationViewCoordinator<Self> {
        return NavigationViewCoordinator(self)
    }
}
  • There should be a .root method for when the Input is a complex type, for example, a tuple. I've had to use the .root(keypath:, input:, comparator:) method and default the closure to { _, _, in true} for it to work.

  • If possible, it would be good to reference the route and pass the parameters directly: .route(to: \.someDefinedRoute(id: viewmodel.id)

  • It would be good to have the TabViewRouter have a reference to the activeTab index, in case the app has custom views/events that rely on this. I'm currently sending the child as an environment object as a workaround.

TabView/Coordinatable - Notify children of tab selection

It's a common feature in many apps that use a tab view with navigation view children where if the tab is tapped twice/multiple times it will pop the navigation view to the root. SwiftUI alone doesn't have this functionality but has a workaround from some people: https://stackoverflow.com/questions/60690933/swiftui-pop-to-root-view-when-selected-tab-is-tapped-again

While that example is very specific, it would be helpful to create a handler that children views of TabCoordinatable will use to detect whether the tab was selected. It is then up to the children to determine what they want to do (like popping the navigation view to root or whatever they may want).

This is a large ask but may also urge me to learn more about how Stinsen works and contribute.

Badge Support

The current TabCoordinatable implementation seams to miss badge() support.
With iOS 15 this would look like this:

TabView {
    Text("Your home screen here")
        .tabItem {
            Label("Home", systemImage: "house")
        }
        .badge(5)
} 

I tried to use the badge modifier with Stinsen, but combining it with a NavigationCoordinatable doesn't work.

What I tried and what didn't work:

Adding the badge in the TabItem view:

 @ViewBuilder func makeTodosTab(isActive: Bool) -> some View {
        Image(systemName: "folder" + (isActive ? ".fill" : ""))
        Text("Todos").badge(5)
  }

Adding the badge on the "Screen" view, this works WITHOUT a NavigationCoordinatable.

struct TodosScreen: View {
    @ViewBuilder var content: some View {
        Color.white
         .badge(5)
        .navigationTitle(with: "Todos")
    }

Basically the only real way to add badges would be to add it directly in the TabView:

TabView(selection: $child.activeTab) {
                    ForEach(Array(views.enumerated()), id: \.offset) { view in
                        view
                            .element
                            .tabItem {
                                coordinator.child.allItems[view.offset].tabItem(view.offset == child.activeTab)
                            }
                            .badge(2)
                            .tag(view.offset)
                    }
                }

What would be the best solution for this?
Making a TabItem Model with a badge and a view?
Making a view.element customize option where we could add the badge() or other modifiers?

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?

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.

FullScreenPresentation always has a solid Background

Hello,
I found a little issue I cloud not figure out myself completely and hope you can help me.
We have the following setup:

  • We have an overview which calls a detail view which works fine with coordinators
  • From the detail view we want to present another view as a fullscreenCover, which also works fine
  • However the fullscreenCover needs to have a transparent Background because we need to see the DetailsView behind a Blur. This works fine when using the SwiftUI built in .fullscreen by Setting our fullscreenCover's background to blur, but using Stinsen there is always a solid BackgroundColor which seems to depend on Light- or Darkmode.

I built an example (see below) where I highlighted the Controller with the solid background. First indications are that the AnyView used in NavigationCoordinatableView does it but I currently have no clue how to adjust this.

Bildschirmfoto 2022-03-16 um 10 53 03

I hope you can help me to figure out how to achieve this.

Best regards

[Question] SplitView

A SplitView is basically just a NavigationView with two root views:
https://www.hackingwithswift.com/books/ios-swiftui/working-with-two-side-by-side-views-in-swiftui

I tried putting this in a NavigationCoordinatable:

@ViewBuilder func start() -> some View {
       Text("Hello, World!")
           .navigationBarTitle("Primary")

       Text("Secondary")
   }

NavigationViewCoordinatableView returns a single view, causing NavigationView not to handle the SplitView correctly.

Have you used Stinsen with a SplitView already?
Is there another solution for this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.