Protocol oriented, Cocoa UI abstractions based library that helps to handle view controllers composition, navigation and deep linking tasks in the iOS application. Can be used as the universal replacement for the Coordinator pattern.

Overview

RouteComposer

CI Status Release Cocoapods Swift Package Manager SwiftUI Carthage compatible Swift 5.2 Platform iOS Documentation Code coverage Codacy Badge MIT License Twitter

RouteComposer is the protocol oriented, Cocoa UI abstractions based library that helps to handle view controllers composition, navigation and deep linking tasks in the iOS application.

Can be used as the universal replacement for the Coordinator pattern.

Table of contents

Navigation concerns

There are 2 ways of implementing the navigation available in the iOS application:

  • Built-in mechanism provided by Apple using storyboards and segues
  • Programmatic navigation directly in the code

The downsides of these two solutions:

  • Built-in mechanism: navigation in the storyboards is relatively static and often requires the extra navigation code in the UIViewControllers and can lead to a lot of boilerplate code
  • Programmatic navigation: forces UIViewControllers coupling or can be complex depending on the chosen design pattern (Router, Coordinator)

RouteComposer helps

  • Facilitate the cutting of an application into small logical steps of navigation
  • Provide the navigation configuration in a declarative way and address the majority of the navigation cases
  • Remove navigation code from UIViewControllers
  • Allow the composition of the UIViewControllers in different ways according to the application state
  • Make every UIViewController deep-linkable out of the box
  • Simplify the creation of the User facing A/B tests with the different navigation and layout patterns
  • Able to work side-by-side with any other navigation mechanism that exist in the IOs application: Builtin or custom

Installation

RouteComposer is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'RouteComposer'

For XCode 10.1 / Swift 4.2 Support

pod 'RouteComposer', '~> 1.4'

And then run pod install.

Once successfully integrated, just add the following statement to any Swift file where you want to use RouteComposer:

import RouteComposer

Check out the Example app included, as it covers most of the general use cases.

Example

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

Requirements

There are no actual requirements to use this library. But if you are going to implement your custom containers and actions you should be familiar with the library concepts and UIKit's view controller navigation stack intricacies.

API documentation

Detailed API documentation can be found here. Test coverage - here

Testimonials

Viz.ai

At Viz.ai, the leading synchronised stroke care service, we went into replacing our entire navigation system, and we knew we needed to address complex and dynamic navigation scenarios. Coordinators and other flow-control libraries just didn't answer our needs, and lead to mixing application logic and navigation, or creating massive coordinator classes. RouteComposer was an amazing fit for us, and actually, as the creator of this library states, it is the drop in replacement for any coordinator code you currently use.

The separation of concerns on this library is absolutely beautiful, and as with anything genius, it all works like magic. It does have a small learning curve, but one that pays off far more than coordinators and flow controllers, and will save you a ton of coding once you implement it.

It makes navigation in the app as simple as saying "go to x with y" and not worrying about the current state or stack. I wholeheartedly recommend it.

Elazar Yifrach, Sr iOS Developer @ Viz.ai

Hudson's Bay Company

In our iOS app we wanted to provide a seamless experience for our users to guarantee that whenever they click on a push notification or a link in an email, they will land on the required view in the app seamlessly no matter of the state of the app.

We tried a programmatic navigational approach in the code and also tried to rely on a few other libraries. However, none of them seemed to do the trick. RouteComposer was not our first choice as originally it looked too complex. Thankfully, it turned out to be a fantastic and elegant solution. We started to use it not only to handle external deeplinking but also to handle our internal navigation within the app.It also turned out to be a great tool for UI A/B tests when you have different navigation patterns for different users. It saved us a load of time, and we really like the logic behind it.

The creator of the library is super responsive and helped with all questions that we had. I would thoroughly recommend it!

Alexandra Mikhailouskaya, Senior lead engineer @ Hudson's Bay Company

Usage

RouteComposer uses 3 main entities (Factory, Finder, Action) that should be defined by a host application to support it. It also provides 3 helping entities (RoutingInterceptor, ContextTask, PostRoutingTask) that you may implement to handle some default actions during the routing process. There are 2 associatedtype in the description of each entity below:

  • ViewController - Type of view controller. UINavigationController, CustomViewController, etc.
  • Context - Type of context object that is passed to the router from the hosting application that router will pass to the view controllers it is going to build. String, UUID, Any, etc. Can be optional.

NB

Context represents a payload that you need to pass to your UIViewController and something that distinguishes it from others. It is not a View Model or some kind of Presenter. It is the missing piece of information. If your view controller requires a productID to display its content, and the productID is a UUID, then the type of Context is the UUID. The internal logic belongs to the view controller. Context answers the questions What to I need to present a ProductViewController and Am I already presenting a ProductViewController for this product.

Implementation

1. Factory

Factory is responsible for building view controllers, that the router has to navigate to upon request. Every Factory instance must implement the Factory protocol:

public protocol Factory {

    associatedtype ViewController: UIViewController

    associatedtype Context

    func build(with context: Context) throws -> ViewController

}

The most important function here is build which should actually create the view controller. For detailed information see the documentation. The prepare function provides you with a way of doing something before the routing actually takes place. For example, you could throw from inside this function in order to inform the router that you do not have the data required to display the view correctly. It may be useful if you are implementing Universal Links in your application and the routing can't be handled, in which case the application might open the provided URL in Safari instead.

Example: Basic implementation of the factory for some custom ProductViewController view controller might look like:

class ProductViewControllerFactory: Factory {

    func build(with productID: UUID) throws -> ProductViewController {
        let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil)
        productViewController.productID = productID // Parameter initialisation can be handled by a ContextAction, see below:

        return productViewController
    }

}

Important note: Automatic associatedtype resolution is broken in XCode 10.2, you must set associated types manually using typealias keyword. Swift compiler bug reported.

2. Finder

Finder helps router to find out if a particular view controller is already present in view controller stack. All the finder instances should conform to Finder protocol.

public protocol Finder {

    associatedtype ViewController: UIViewController

    associatedtype Context

    func findViewController(with context: Context) throws -> ViewController?

}

In some cases, you may use default finders provided by the library. In other cases, when you can have more than one view controller of the same type in the stack, you may implement your own finder. There is an implementation of this protocol included called StackIteratingFinder that helps to solve iterations in view controller stack and handles it. You just have to implement the function isTarget to determine if it's the view controller that you are looking for or not.

Example of ProductViewControllerFinder that can help the router find a ProductViewController that presents a particular product in your view controller stack:

class ProductViewControllerFinder: StackIteratingFinder {

    let iterator: StackIterator = DefaultStackIterator()

    func isTarget(_ productViewController: ProductViewController, with productID: UUID) -> Bool {
        return productViewController.productID == productID
    }

}

SearchOptions is an enum that informs StackIteratingFinder how to iterate through the stack when searching. See documentation.

3. Action

The Action instance explains to the router how the view controller is created by a Factory should be integrated into a view controller stack. Most likely, you will not need to implement your own actions because the library provides actions for most of the default actions that can be done in UIKit like (GeneralAction.presentModally, UITabBarController.add, UINavigationController.push etc.). You may need to implement your own actions if you are doing something unusual.

Check example app to see a custom action implementation.

Example: As you most likely will not need to implement your own actions, let's look at the implementation of PresentModally provided by the library:

class PresentModally: Action {

    func perform(viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (_: RoutingResult) -> Void) {
        existingController.present(viewController, animated: animated, completion: {
            completion(.success)
        })
    }

}

4. Routing Interceptor

Routing interceptor will be used by the router before it will start routing to the target view controller. For example, to navigate to some particular view controller, the user might need to be logged in. You may create a class that implements the RoutingInterceptor protocol and if the user is not logged in, it will present a login view controller where the user can log in. If this process finishes successfully, the interceptor should inform the router and it will continue routing or otherwise stop routing. See example app for details.

Example: If the user is logged in, router can continue routing. If the user is not logged in, the router should not continue

class LoginInterceptor<C>: RoutingInterceptor {

    func perform(with context: C, completion: @escaping (_: RoutingResult) -> Void) {
        guard !LoginManager.sharedInstance.isUserLoggedIn else {
            completion(.failure("User has not been logged in."))
            return
            // Or present the LoginViewController. See Example app for more information. 
        }
        completion(.success)
    }

}

5. Context Task

If you are using one default Factory and Finder implementation provided by the library, you still need to set data in context to your view controller. You have to do this even if it already exists in the stack, if it's just going to be created by a Factory or do any other actions at the moment when router found/created a view controller. Just implement ContextTask protocol.

Example: Even if ProductViewController is present on the screen or it is going to be created you have to set productID to present a product.

class ProductViewControllerContextTask: ContextTask {

    func perform(on productViewController: ProductViewController, with productID: UUID) {
        productViewController.productID = productID
    }

}

See example app for the details.

Or use ContextSettingTask provided with the library to avoid extra code.

6. Post Routing Task

A post-routing task will be called by the router after it successfully finishes navigating to the target view controller. You should implement PostRoutingTask protocol and create all necessary actions there.

Example: You need to log an event in your analytics every time the user lands on a product view controller:

class ProductViewControllerPostTask: PostRoutingTask {

    let analyticsManager: AnalyticsManager

    init(analyticsManager: AnalyticsManager) {
        self.analyticsManager = analyticsManager
    }

    func perform(on productViewController: ProductViewController, with productID: UUID, routingStack: [UIViewController]) {
        analyticsManager.trackProductView(productID: productViewController.productID)
    }

}

Configuring Step

Everything that the router does is configured using a DestinationStep instance. There is no need to create your own implementation of this protocol. Use StepAssembly provided by the library to configure any step that the router should execute during the routing.

Example: A ProductViewController configuration that explains to the router that it should be boxed in UINavigationController which should be presented modally from any currently visible view controller.

let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory())
        .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719, https://bugs.swift.org/browse/SR-8705 are fixed
        .add(ProductViewControllerContextTask())
        .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance))
        .using(UINavigationController.push())
        .from(NavigationControllerStep())
        .using(GeneralActions.presentModally())
        .from(GeneralStep.current())
        .assemble()

This configuration means:

  • Use ProductViewControllerFinder to potentially find an existing product view controller in the stack, or create it using ProductViewControllerFactory if it has not been found.
  • If it was created push it into a navigation stack
  • Navigation stack should be provided from another step NavigationControllerStep, that will create a UINavigationController instance
  • The UINavigationController instance should be presented modally from any currently visible view controller.
  • Before routing run LoginInterceptor
  • After view controller been created or found, run ProductViewControllerContextTask
  • After successful routing run ProductViewControllerPostTask

See example app to find out different ways to provide and store routing step configurations.

See advanced ProductViewController configuration here.

Navigation

After you have implemented all necessary classes and configured a routing step, you can start to use the Router to navigate. The library provides a DefaultRouter which is an implementation of the Router protocol to handle routing based on the configuration explained above.

Example: The user taps on a cell in a UITableView. It then asks the router to navigate the user to ProductViewController. The user should be logged into see the product details.

struct Configuration {

    static let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory())
                .add(LoginInterceptor<UUID>())
                .add(ProductViewControllerContextTask())
                .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance))
                .using(UINavigationController.push())
                .from(NavigationControllerStep())
                .using(GeneralActions.presentModally())
                .from(GeneralStep.current())
                .assemble()

}

class ProductArrayViewController: UITableViewController {

    let products: [UUID]?

    let router = DefaultRouter()

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let productID = products[indexPath.row] else {
            return
        }
        try? router.navigate(to: Configuration.productScreen, with: productID)
    }

}

Example below shows the same process without the use of RouteComposer

class ProductArrayViewController: UITableViewController {

    let products: [UUID]?

    let analyticsManager = AnalyticsManager.sharedInstance

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let productID = products[indexPath.row] else {
            return
        }

        // Handled by LoginInterceptor
        guard !LoginManager.sharedInstance.isUserLoggedIn else {
            return
        }

        // Handled by a ProductViewControllerFactory
        let productViewController = ProductViewController(nibName: "ProductViewController", bundle: nil)

        // Handled by ProductViewControllerContextTask
        productViewController.productID = productID

        // Handled by NavigationControllerStep and UINavigationController.push
        let navigationController = UINavigationController(rootViewController: productViewController)

        // handled by DefaultActions.PresentModally
        present(navigationController, animated: true) { [weak self]
            // Handled by ProductViewControllerPostTask
            self?.analyticsManager.trackProductView(productID: productID)
        }
    }

}

In the example without RouteComposer the code may seem simpler, however, everything is hardcoded in the actual function implementation. RouteComposer allows you to split everything into small reusable pieces and store navigation configuration separately from your view logic. Also, the above implementation will grow dramatically when you try to add Universal Link support to your app. Especially if you will have to choose from opening ProductViewController from a universal link if it is already present on the screen or not and so on. With the library, each of your view controllers is deep linkable by nature.

As you can see from the examples above the Router does not do anything that tweaks UIKit basis. It just allows you to break the navigation process into small reusable pieces. The router will call them in a proper order based on the configuration provided. The library does not break the rules of VIPER or MVVM architectural patterns and can be used in parallel with them.

See example app for other examples of defining routing configurations and instantiating router.

Deep-linking

With RouteComposer every view controller becomes deep-linkable out of the box. You can also provide different configuration in case the screen is being opened using universal link. See Example app for more information.

    let router = DefaultRouter()

    func application(_ application: UIApplication,
                     open url: URL,
                     sourceApplication: String?,
                     annotation: Any) -> Bool {
        guard let productID = extractProductId(from: url) else {
            return false
        }
        try? router.navigate(to: Configuration.productScreen, with: productID)
        return true
    }

Troubleshooting

If for some reason you are unsatisfied with the result and you think that it is the Routers issue, or you found that your particular case is not covered, you can always temporarily replace the router with your custom implementation and implement simple routing yourself. Please, create a new issue and we will try to fix the issue as soon as possible.

Example:

     func goToProduct(with productId: UUID) {
        // If view controller with this product id is present on the screen - do nothing
        guard ProductViewControllerFinder(options: .currentVisibleOnly).getViewController(with: productId) == nil else {
            return
        }
        
        /// Otherwise, find visible `UINavigationController`, build `ProductViewController`
        guard let navigationController = ClassFinder<UINavigationController, Any?>(options: .currentVisibleOnly).getViewController(),
              let productController = try? ProductViewControllerFactory().execute(with: productId) else {
            return
        }
        
        /// Apply context task if necessary
        try? ProductViewControllerContextTask().execute(on: productController, with: productId)

        /// Push `ProductViewController` into `UINavigationController`
        navigationController.pushViewController(productController, animated: true)
    }

SwiftUI:

RouteComposer is compatible with SwiftUI. See example app for the details.

Advanced Configuration:

You can find more configuration examples here.

Contributing

RouteComposer is in active development, and we welcome your contributions.

If you’d like to contribute to this repo, please read the contribution guidelines.

License

RouteComposer is distributed under the MIT license.

RouteComposer is provided for your use, free-of-charge, on an as-is basis. We make no guarantees, promises or apologies. Caveat developer.

Articles

English:

Russian:

Author

Evgeny Kazaev, [email protected]. Twitter ekazaev

I am happy to answer any questions you may have. Just create a new issue.

Comments
  • Route Composer does not find a ViewController inside a tab bar when it is being pushed

    Route Composer does not find a ViewController inside a tab bar when it is being pushed

    I really like your framework. It works great. However I found a small bug.

    I have the following setup:

    /// Selector scene
    UINavigationController(Nav1):
    	AccountSelectorViewController
    
    Pushes: ->
    
    // Home Scene
    UITabBar:
    	UINavigationController:
    		GreenViewController
    	UINavigationController:
    		RedViewController
    

    Each ViewController has NavigationContext object that is Equatable. This context is set on the TabBar too.

    When tabbar is pushed, first NavigationController (Nav1) is being hidden.
    When tabbar is popped, first NavigationController(Nav1) is shown again

    Here is routes configuration:

    struct Path {
    
        static var accountSelector: DestinationStep<AccountSelectorViewController, NavigationContext?> {
            return StepAssembly(
                finder: ClassFinder<AccountSelectorViewController, NavigationContext?>(),
                factory: ClassFactory<AccountSelectorViewController, NavigationContext?>())
                .using(UINavigationController.push())
                .from(NavigationControllerStep())
                .using(GeneralAction.replaceRoot())
                .from(GeneralStep.current())
                .assemble()
        }
    
        static var accountHome: DestinationStep <TabbarViewController, NavigationContext?> {
            return StepAssembly(
                finder: ClassWithContextFinder<TabbarViewController, NavigationContext?>(),
                factory: TabBarFactory())
                .using(UINavigationController.push())
                .from(accountSelector.expectingContainer())
                .assemble()
        }
    
        static var green: DestinationStep <GreenViewController, NavigationContext?> {
            return StepAssembly(
                finder: ClassWithContextFinder<GreenViewController, NavigationContext?>(),
                factory: NilFactory())
                .from(Path.accountHome)
                .assemble()
        }
    
    
        static var red: DestinationStep <RedViewController, NavigationContext?> {
            return StepAssembly(
                finder: ClassWithContextFinder<RedViewController, NavigationContext?>(),
                factory: NilFactory())
                .from(Path.accountHome)
                .assemble()
        }
        
    }
    
    

    The bug happens when RedViewController is selected and app tries to deeplink to RedViewController but with a different context.

    Here is full console output when this happens:

    [Router] Started to search for the view controller to start the navigation process from.
    
    [Router] BaseStep<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : FinderFactory<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>>(configuration: nil, finder: RouteComposer.ClassWithContextFinder<RouteComposerBug.RedViewController, Swift.Optional<RouteComposerBug.NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator()))))> hasn't found a corresponding view controller in the stack, so it will be built using FinderFactory<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>>(configuration: nil, finder: RouteComposer.ClassWithContextFinder<RouteComposerBug.RedViewController, Swift.Optional<RouteComposerBug.NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator()))).
    
    [Router] BaseStep<ClassWithContextFinder<TabbarViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : TabBarFactory())> hasn't found a corresponding view controller in the stack, so it will be built using TabBarFactory().
    
    [Router] BaseStep<ClassFinder<AccountSelectorViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : ClassFactory<AccountSelectorViewController, Optional<NavigationContext>>(nibName: nil, bundle: nil, configuration: nil))> found <RouteComposerBug.AccountSelectorViewController: 0x7f94808049d0> to start the navigation process from.
    
    [Router] Started to build the view controllers stack.
    
    [Router] TabBarFactory() built a <RouteComposerBug.TabbarViewController: 0x7f9482847a00>.
    
    [Router] PushAction<UINavigationController>() has applied to <RouteComposerBug.AccountSelectorViewController: 0x7f94808049d0> with <RouteComposerBug.TabbarViewController: 0x7f9482847a00>.
    
    [Router] Composition Failed Error: ClassWithContextFinder<RedViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) hasn't found its view controller in the stack.
    
    [Router] Unsuccessfully finished the navigation process.
    
    bug 
    opened by 0rtm 22
  • Pass a different type of context to children

    Pass a different type of context to children

    Hello, I would like to push a viewController from another but with different contexts. I can't figure out how to do this. I have a first screen embedded in my tabBar:

        var firstScreen: DestinationStep<FirstViewController, FirstContext> {
            StepAssembly(
                finder: FirstViewControllerFinder(),
                factory: FirstViewControllerFactory()
            )
            .using(UITabBarController.add())
            .from(homeScreen.expectingContainer())
            .assemble()
        }
    

    And I am trying to push a second viewController from it:

        var secondScreen: DestinationStep<SecondViewController, SecondContext> {
            StepAssembly(
                finder: SecondViewControllerFinder(),
                factory: SecondViewControllerFactory()
            )
            .using(UINavigationController.push())
            .assemble(from: firstScreen.expectingContainer())
        }
    

    But I have an error because FirstContext and SecondContext are not the same. Is there a way to solve this case ?

    Thank you.

    question 
    opened by clementleys 16
  • How to change root view controller using LoginInterceptor and then continue navigation

    How to change root view controller using LoginInterceptor and then continue navigation

    Hi!

    I am trying to understand how to compose the navigation for the following case:

    • user taps on deep link outside the app (cold boot) to a destination behind a login interceptor
    • app opens on splash (landing) and navigates to login (due to interceptor)
    • login succeeds, root is changed from splash to home
    • destination is shown

    I've tried to navigate to home in the interceptor, but the subsequent navigation to destination fails, because landing was used for origin view controller.

    Any ideas?

    P.S.: I'm trying to do this in the example app with the ColorViewController. Here is the diff:

    index 12d2bee8..c42e0b3f 100644
    --- a/Example/RouteComposer/Configuration/ExampleConfiguration.swift
    +++ b/Example/RouteComposer/Configuration/ExampleConfiguration.swift
    @@ -78,6 +78,7 @@ extension ExampleScreenConfiguration {
             StepAssembly(
                 finder: ColorViewControllerFinder(),
                 factory: ColorViewControllerFactory())
    +            .adding(LoginInterceptor<String>())
                 .adding(DismissalMethodProvidingContextTask(dismissalBlock: { context, animated, completion in
                     // Demonstrates ability to provide a dismissal method in the configuration using `DismissalMethodProvidingContextTask`
                     UIViewController.router.commitNavigation(to: GeneralStep.custom(using: PresentingFinder()), with: context, animated: animated, completion: completion)
    
    index 97e20638..0a3dff78 100644
    --- a/Example/RouteComposer/SceneDelegate.swift
    +++ b/Example/RouteComposer/SceneDelegate.swift
    @@ -18,6 +18,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
             ConfigurationHolder.configuration = ExampleConfiguration()
     
    +      guard let windowScene = (scene as? UIWindowScene) else { return }
    +
    +      /// 2. Create a new UIWindow using the windowScene constructor which takes in a window scene.
    +      let window = UIWindow(windowScene: windowScene)
    +
    +      let storyboard = UIStoryboard(name: "PromptScreen", bundle: nil)
    +      let controller = storyboard.instantiateInitialViewController()
    +
    +      window.rootViewController = controller
    +      self.window = window
    +      
             // Try in mobile Safari to test the deep linking to the app:
             // Try it when you are on any screen in the app to check that you will always land where you have to be
             // depending on the configuration provided.
    @@ -26,8 +37,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
             // dll://products?product=01
             // dll://cities?city=01
             ExampleUniversalLinksManager.configure()
    +
    +      window.makeKeyAndVisible()
    
    help wanted 
    opened by tadelv 13
  • Help with some switch assembly...

    Help with some switch assembly...

    Hi, I'd really love some help in the following scenario, I couldn't seem to figure it out myself or with the docs, any help would be greatly appreciated:

    assuming I have: ConversationsListViewController: UIViewController ConversationViewController: UIViewController ConversationContext (the context for both the above)

    and I want this scenario for presenting the ConversationViewController:

    if current visible view controller is contained in a navigation controller, where the root view controller is ConversationsListViewController {
        pop the navigation controller to root
        push ConversationViewController
    } else if visible view controller is contained in any other navigation controller {
        just push ConversationViewController to the navigation controller
    } else {
        create a navigation controller
        present it modally
        push ConversationViewController to it
    }
    

    I think I'm good with writing the action for popping a navigation controller to root, i'm just not really sure where that goes.

    static let conversation = StepAssembly(finder: ConversationViewControllerFinder(),
                                             factory: ConversationViewControllerFactory())
    .using(UINavigationController.push())
    .from(SwitchAssembly()
        .addCase(when: ClassFinder<ConversationsListViewController, ConversationContext>(options: .currentAndDown),
                 from: /* I BELIEVE THIS IS THE PART I'M MISSING */ )
        .addCase(from: ClassFinder<UINavigationController, ConversationContext>(options: .currentVisibleOnly))
        .assemble(default: ChainAssembly
            .from(NavigationControllerStep<UINavigationController, ConversationContext>())
            .using(GeneralAction.presentModally())
            .from(GeneralStep.current())
            .assemble()))
    .assemble()
    
    bug help wanted 
    opened by Elaz-viz 12
  • Why can't the second step in a UINavigationController use a different context than the first step?

    Why can't the second step in a UINavigationController use a different context than the first step?

    My goal is to setup a navigation controller and have a series of view controllers that I can push onto the navigation controller stack. I feel like I'm missing something obvious because this should be very simple. I'd like to pass a unique identifier Int? to the first view controller, instead of using Any? as the context.

        static var firstStep = StepAssembly(
            finder: ClassFinder(),
            factory: ClassFactory<FirstViewController, Int?>())
            .using(UINavigationController.push())
            .from(NavigationControllerStep<UINavigationController, Int?>())
            .using(GeneralAction.replaceRoot())
            .from(GeneralStep.root())
            .assemble()
        
        static var secondStep = StepAssembly(
            finder: ClassFinder(),
            factory: ClassFactory<SecondViewController, Any?>())
            .using(UINavigationController.push())
            .from(firstStep.expectingContainer())
            .assemble()
    

    This does not compile, the compiler complains that it can't convert Int? to Any? and that Generic parameter 'VC' could not be inferred.

    question 
    opened by bennnjamin 10
  • UINavigationController push animations breaking

    UINavigationController push animations breaking

    After a couple of routings have been successfully performed, animations stop working for all navigation controller pushes. I can't tell what causes it because it seems to begin randomly, but thereafter it's an app-wide problem until I restart the app. Basic app structure is a tab bar controller with split view controllers in some of the tabs. I'm on iOS 13 beta and Xcode 11.

    invalid question 
    opened by benronicus 10
  • Assertion fires in UIKit app when built for previewing SwiftUI View

    Assertion fires in UIKit app when built for previewing SwiftUI View

    When integrating a SwiftUI view in an existing UIKit app, the preview hangs and errors with timeout. The diagnostics point to this crash: https://github.com/ekazaev/route-composer/blob/8eeab58be64ef6d7a81f59b77f759a7618173df4/RouteComposer/Classes/Finders/Stack%20Iterator/KeyWindowProvider.swift#L29 Commenting out the assertion (and just returning nil) clears the issue and the preview works, but I'm not sure if that's a good solution...

    enhancement 
    opened by Elaz-viz 8
  • Route Composer does not show modal view controller presented from tabbar when tabbar is pushed

    Route Composer does not show modal view controller presented from tabbar when tabbar is pushed

    Here is the repo containing code with steps to reproduce the bug: https://github.com/0rtm/RouteComposerBug

    To reproduce the bug run the following commands:

    xcrun simctl openurl booted rcbug://modal?account=Account1
    xcrun simctl openurl booted rcbug://modal?account=Account2
    

    Setup is same as in bug 33 but with addition of showing modal view controller from one tab

     /// Selector scene
    UINavigationController(Nav1): AccountSelectorViewController
       
    Pushes: ->
    
    // Home Scene 
    UITabBar:
    	UINavigationController:
    		GreenViewController
    	UINavigationController:
    		RedViewController 
          	presents -->(Modal) ModalViewController 
    

    Route composer is unable to navigate to Modal View Controller when context is different.

    Here is the console output:

    [Router] Started to search for the view controller to start the navigation process from.
    
    [Router] BaseStep<ClassWithContextFinder<ModalVCViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : ClassFactory<ModalVCViewController, Optional<NavigationContext>>(nibName: nil, bundle: nil, configuration: nil))> hasn't found a corresponding view controller in the stack, so it will be built using ClassFactory<ModalVCViewController, Optional<NavigationContext>>(nibName: nil, bundle: nil, configuration: nil).
    
    [Router] BaseStep<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : FinderFactory<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>>(configuration: nil, finder: RouteComposer.ClassWithContextFinder<RouteComposerBug.RedViewController, Swift.Optional<RouteComposerBug.NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator()))))> hasn't found a corresponding view controller in the stack, so it will be built using FinderFactory<ClassWithContextFinder<RedViewController, Optional<NavigationContext>>>(configuration: nil, finder: RouteComposer.ClassWithContextFinder<RouteComposerBug.RedViewController, Swift.Optional<RouteComposerBug.NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator()))).
    
    [Router] BaseStep<ClassWithContextFinder<TabbarViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : TabBarFactory())> hasn't found a corresponding view controller in the stack, so it will be built using TabBarFactory(). 
    
    [Router] BaseStep<ClassFinder<AccountSelectorViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) : ClassFactory<AccountSelectorViewController, Optional<NavigationContext>>(nibName: nil, bundle: nil, configuration: nil))> found <RouteComposerBug.AccountSelectorViewController: 0x7fa079905e90> to start the navigation process from.
    
    [Router] Started to build the view controllers stack.
    
    [Router] TabBarFactory() built a <RouteComposerBug.TabbarViewController: 0x7fa078828400>. 
    
    [Router] CATransactionWrappedContainerAction<PushAction<UINavigationController>>(action: RouteComposer.NavigationControllerActions.PushAction<__C.UINavigationController>()) has applied to <RouteComposerBug.AccountSelectorViewController: 0x7fa079905e90> with <RouteComposerBug.TabbarViewController: 0x7fa078828400>.
    
    [Router] Composition Failed Error: ClassWithContextFinder<RedViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())) hasn't found its view controller in the stack. 
    
    [Router] Unsuccessfully finished the navigation process. 
    
    bug wontfix 
    opened by 0rtm 8
  • When and why to define Steps as computed property, static var, static func and let

    When and why to define Steps as computed property, static var, static func and let

    I am new to this library and looking through the Example Project is a great place to start, it's very well documented. One thing that I have had trouble understanding and doesn't seem obvious is why Steps are defined in a variety of ways. Sometimes they are static vars/funcs, sometimes they are computed properties, and sometimes they are let constants.

    What is your thinking in using these different approaches to defining routes? Why shouldn't every route just be static let so they can refer to each other if needed, and be accessible from every view controller?

    question 
    opened by bennnjamin 7
  • Modal ViewController is not dismissed when presented using overCurrentContext style

    Modal ViewController is not dismissed when presented using overCurrentContext style

    When a ViewController is presented modally using overCurrentContext style, it is not dismissed by route-composer when further navigation happens.

    Problem happens only if I use modal presentation over current context

     .using(GeneralAction.presentModally(presentationStyle: .overCurrentContext,
                                                   transitionStyle: .crossDissolve))
    

    In case if same DestinationStep uses fullScreen instead of overCurrentContext presentation style the issue does not occur.

    Link to the repo with issue: https://github.com/0rtm/RouteComposerBug

    Steps to reproduce:

    0. Check out repo, update pods
    1. xcrun simctl openurl booted rcbug://overlay?account=Account2 (may need to run this twice)
    2. Click green navigate button
    3. Go back to history tab
    

    Modal screen with 2 buttons should disappear. As if you run:

    xcrun simctl openurl booted rcbug://overlay2?account=Account2 
    
    enhancement help wanted 
    opened by 0rtm 7
  • Change tabs in TabBar in runtime

    Change tabs in TabBar in runtime

    I use this code to init tab bar with 3 tabs – First, Second and Profile:

        var tabBarScreen: DestinationStep<UITabBarController, Any?> {
            StepAssembly(
                finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root),
                factory: Configuration.completeFactory)
                .using(GeneralAction.replaceRoot())
                .from(GeneralStep.root())
                .assemble()
        }
    ...
    
       static let tabBarFactory = CompleteFactoryAssembly(
                   factory: TabBarControllerFactory(nibName: nil, bundle: nil, delegate: nil))
            .with(firstAssemble)
            .with(secondAssemble)
            .with(profileAssemble)
            .assemble()
    

    After the user opens the Second tab bar the first time and finished some flow, how can I change in runtime Second flow for the second tab to another flow, like Second_v2?

    help wanted 
    opened by snowtema 6
Releases(2.10.2)
Owner
Eugene Kazaev
Eugene Kazaev
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 538 Dec 8, 2022
A splendid route-matching, block-based way to handle your deep links.

DeepLink Kit Overview DeepLink Kit is a splendid route-matching, block-based way to handle your deep links. Rather than decide how to format your URLs

Button 3.4k Dec 30, 2022
Interface-oriented router for discovering modules, and injecting dependencies with protocol in Objective-C and Swift.

ZIKRouter An interface-oriented router for managing modules and injecting dependencies with protocol. The view router can perform all navigation types

Zuik 631 Dec 26, 2022
⛵️ URLNavigator provides an elegant way to navigate through view controllers by URLs.

URLNavigator ⛵️ URLNavigator provides an elegant way to navigate through view controllers by URLs. URL patterns can be mapped by using URLNavigator.re

Suyeol Jeon 2.6k May 27, 2021
📱📲 Navigate between view controllers with ease. 💫 🔜 More stable version (written in Swift 5) coming soon.

CoreNavigation ?? ?? Navigate between view controllers with ease. ?? ?? More stable version (written in Swift 5) coming soon. Getting Started API Refe

Aron Balog 69 Sep 21, 2022
🍞 [Beta] A view controller that can unwind like presentation and navigation.

FluidPresentation - no more handling presented or pushed in view controller A view controller that supports the interactive dismissal by edge pan gest

Muukii 19 Dec 22, 2021
A simple, powerful and elegant implementation of the coordinator template in Swift for UIKit

A simple, powerful and elegant implementation of the coordinator template in Swift for UIKit Installation Swift Package Manager https://github.com/bar

Aleksei Artemev 22 Oct 16, 2022
A compact Coordinator from XCoordinator

A lightweight navigation framework based on the Coordinator pattern and is a compact version from XCoordinator.

Duc Pham 3 Jul 9, 2022
🎯Linker Lightweight way to handle internal and external deeplinks in Swift for iOS

Linker Lightweight way to handle internal and external deeplinks in Swift for iOS. Installation Dependency Managers CocoaPods CocoaPods is a dependenc

Maksim Kurpa 128 May 20, 2021
A deep copy of Pinterest in Swift

Demo YouTube: Demo (2 minutes) 优酷:http://v.youku.com/v_show/id_XMzE3OTc5NDY2MA==.html?spm=a2h3j.8428770.3416059.1 The app is actually smoother than sh

Andy Tong 73 Sep 14, 2022
An easier way to handle third-party URL schemes in iOS apps.

IntentKit ========= IntentKit is an easier way to handle third-party URL schemes in iOS apps. Linking to third-party apps is essentially broken on iOS

null 1.8k Dec 24, 2022
Easy and maintainable app navigation with path based routing for SwiftUI.

Easy and maintainable app navigation with path based routing for SwiftUI.

Freek Zijlmans 278 Jun 7, 2021
🛣 Simple Navigation for iOS

Router Reason - Get Started - Installation Why Because classic App Navigation introduces tight coupling between ViewControllers. Complex Apps navigati

Fresh 457 Jan 4, 2023
An extremely lean implementation on the classic iOS router pattern.

Beeline is a very small library that aims to provide a lean, automatic implementation of the classic iOS router pattern.

Tim Oliver 9 Jul 25, 2022
An experimental navigation router for SwiftUI

SwiftUIRouter ?? An ⚠️ experimental ⚠️ navigation router for SwiftUI Usage ?? Check out ExampleApp for more. Define your routes: import SwiftUIRouter

Orkhan Alikhanov 16 Aug 16, 2022
URLScheme router than supports auto creation of UIViewControllers for associated url parameters to allow creation of navigation stacks

IKRouter What does it do? Once you have made your UIViewControllers conform to Routable you can register them with the parameters that they represent

Ian Keen 94 Feb 28, 2022
URL routing library for iOS with a simple block-based API

JLRoutes What is it? JLRoutes is a URL routing library with a simple block-based API. It is designed to make it very easy to handle complex URL scheme

Joel Levin 5.6k Jan 6, 2023
Monarch Router is a Declarative URL- and state-based router written in Swift.

Monarch Router is a declarative routing handler that is capable of managing complex View Controllers hierarchy transitions automatically, decoupling View Controllers from each other via Coordinator and Presenters. It fits right in with Redux style state flow and reactive frameworks.

Eliah Snakin 31 May 19, 2021
DZURLRoute is an Objective-C implementation that supports standard-based URLs for local page jumps.

DZURLRoute Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements s.dependency 'DZVie

yishuiliunian 72 Aug 23, 2022