Coder Social home page Coder Social logo

ekazaev / route-composer Goto Github PK

View Code? Open in Web Editor NEW
885.0 33.0 62.0 63.72 MB

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.

License: MIT License

Ruby 0.20% Swift 99.45% Objective-C 0.35%
router routing-engine deeplink deeplinks universal-links ios factory swift finder swift5

route-composer's People

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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

route-composer's Issues

Make NavigationDelayingInterceptor context generic

Method .adding(...) requires that RI.Context == C, which means factory and interceptor context types should be equal.

This, however, is not always possible with current implementation of NavigationDelayingInterceptor, which has Context = Any?.

Adding a generic parameter Context to NavigationDelayingInterceptor would fix the problem, and it seems easy to do, though I'm not sure how difficult it would be to refactor usages of the interceptor.

need enlightenment from heaven

RouteComposer was awesome, I switched from RxFlow. I'm slowly understanding RouteComposer, can you provide an example/code how to support navigation with below scheme

root(UINavigationController) -> Login -> Register Email -> Register Final (completed some field)

if register final submit and success, I want to back navigation to Login, can i use RouteComposer ?

or

just check

if let viewController = navigationController?.viewControllers.filter({$0 is LoginViewController}).first as? LoginViewController navigationController?.popToViewController(viewController, animated: true)
} else {
navigationController?.popToRootViewController(animated: true)
}

thanks

Finding an improved solution for conditional transition type selection of UIViewController in Swift

I'm trying to implement a transition type depending on the presence of a specific UIViewController with a certain context.

// If I try to return `ContainerAction` (not any), I get an error
// Error: Use of protocol 'ContainerAction' as a type must be written 'any ContainerAction'
func navigationAction() -> any ContainerAction {
    // If the view controller hierarchy already contains an instance of `ClientManagerChatViewController`, 
    // but with a different context
    if let _ = ClassFinder<ClientManagerChatViewController, Context>().getViewController(with: context) {
        // If the view controller hierarchy already contains an instance of `ClientManagerChatViewController` 
        // with the same context
        if let _ = ClassWithContextFinder<ClientManagerChatViewController, Context>().getViewController(with: context) {
            // Do nothing, just return the current UIViewController
            return NavigationController.push()
        } else {
            // Replace the last navigation action with a new one, as the context is different
            return NavigationController.pushReplacingLast()
        }
    } else {
        // If the `ClientManagerChatViewController` isn't present in the hierarchy, perform the push navigation action
        return NavigationController.push()
    }
}

let step = StepAssembly(
    finder: ClassWithContextFinder<ClientManagerChatViewController, Context>(),
    factory: Factory(dependencies: deps)
)
.adding(LoginInterceptor<Context>())
// However, when attempting to use `any ContainerAction` I get an error:
// Error: Type 'any ContainerAction' cannot conform to 'ContainerAction'
.using(navigationAction()) // needs: NavigationController.push() || NavigationController.pushReplacingLast()
.from(GeneralStep.custom(using: ClassFinder<NavigationController, Context>()))
.assemble()

NavigationController it's child of UINavigationController
As a result, I'm returning the entire Destination construction through an if-else statement instead of just selecting the type of navigation action. Could you possibly help me find a better way to solve this?

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

Best way to handle if/else logic when a route depends on two different prior View Controllers

I have a DestinationStep that requires coming from two different places. What's the best way to handle this? Here is some example code for how I thought I could accomplish it:

   let studyTabBarStep = StepAssembly(
        finder: NilFinder<StudyTabBarController, Any?>(),
        factory: StudyTabBarFactory())
        .using(UINavigationController.pushAsRoot())
        .from(GeneralStep.custom(using: ClassFinder<StudyListController, Any?>()).expectingContainer())
        .assemble()
    func showStudy(id: Int) {
        if let _ = ClassFinder<StudyListController, Any?>().getViewController() {
            router.commitNavigation(to: Destination(to: studyTabBarStep, with: StudyContext(id: id), animated: true, completion: nil)
        } else {
            let step = StepAssembly(
                finder: NilFinder<StudyTabBarController, Any?>(),
                factory: StudyTabBarFactory())
                .using(UINavigationController.pushAsRoot())
                .from(NavigationControllerStep<UINavigationController, Any?>())
                .using(GeneralAction.replaceRoot())
                .from(GeneralStep.root())
                .assemble()
            
            router.commitNavigation(to: Destination(to: step, with: StudyContext(id: id), animated: true, completion: nil)
        }
    }

I have a Study list controller, which is the root view controller. When you select a study from the list, it will replace the root view controller with the StudyTabBar. But if you come from a deeplink or notification, the StudyListController might not be on the screen (perhaps the user is on a StudyTabBar screen for a different ID). I need to replace the root view controller with this new StudyTabBar.

I use a Finder to check if the StudyListController exists first, and if it does I can use the Step to replace the view controller with the study. If the view controller doesn't exist, then I create a different step which builds the navigation controller and replaces root with the study tab bar.

Is this a good way to do it or is there a better way?

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.

What is the best way to handle different routes for the same deep link?

Hi, I am trying to setup a deep linking flow that is different based on some state, like if the user is logged in.

The deep link contains the unique identifier for a chat room for example. I would like this route flow:

If the user is logged in, take them to the chat room. Else, show the user the sign up screen to create an account. Is there a way to handle this exclusively using Route Interceptors and different routes? Or do I simply check if they are logged in when handling deep links and then navigate accordingly?

I already have a Route Interceptor to check if the user is logged in and take them to the Login screen if they aren't logged in (the login screen can also route the user to the sign up screen to create an account), but I am not sure how to integrate deep link logic into this so that there are entirely different routes the user takes from the same deep link.

Help finding a contained navcontroller within UISplitView

I can't figure out how to push a detail view onto a split view's current detail view. The split view's detail view hierarchy is a navigation controller with a view, but I can't get the router to find the navigation controller (highlighted in the attached screenshot):

var detailOne: DestinationStep<DetailOneController, DetailContext> {
    return StepAssembly(
      finder: ClassFinder<DetailOneController, DetailContext>(),
      factory: StoryboardFactory(storyboardName: "People", viewControllerID: "DetailOneController"))
      .using(UISplitViewController.pushToDetails())
      .from(peopleSplitView.expectingContainer())
      .assemble()
  }
  
  var detailTwo: DestinationStep<DetailTwoController, DetailContext> {
    return StepAssembly(
      finder: ClassFinder<DetailTwoController, DetailContext>(),
      factory: DetailTwoControllerFactory(dependencies: personDependencies))
      // .using(UINavigationController.push())
      // .from(detailOne.expectingContainer())
      .using(UISplitViewController.pushToDetails())
      .from(peopleSplitView.expectingContainer())
      .assemble()
  }

In the detailTwo assembly, if I use the commented out lines UINavigationController.push from detailOne.expectingContainer, the router can't find the split view's second contained navigationController and says "Type Mismatch Error: Container of UINavigationController type cannot be found to perform PushAction()", and if I instead use UISplitViewController.pushToDetails from peopleSplitView.expectingContainer it replaces the entire detail hierarchy (nav controller and DetailOneController) with a DetailTwoController and then the user can't navigate back to detailOne.

This is only causing me trouble in landscape orientation with a regular horizontal size class (in portrait it pushes on detailTwo just fine).

heirarchy

Hot to close screen/module using Router

Hello! I am facing a problem using RouteComposer in my application. The problem is that the library allows you to define a way to open an arbitrary screen from anywhere in the application, which is awesome, but I did not understand how can i use the library (DefaultRouter) to close a screen that is open in this way. Let me give you an example

I have a default "Login" screen defined as:
let loginStep = StepAssembly(finder: finder, factory: factory) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralAction.presentModally()) .from(GeneralStep.current()) .assemble() in my AppModulesFactory.
When i want to route to auth screen, i use try? router.navigate(to: loginStep, with: nil)
When user taps on "Sign In" button i have to write some code to remove LoginViewController (embeded in UINavigationController) from screen. Now i use the only found solution: try? DefaultRouter().navigate(to: GeneralStep.custom(using: PresentingFinder()), with: ()) from inside my LoginViewController. Although this method works at the moment, this code will break as soon as the definition of how the loginStep is defined change. For example if i change "Embed to UINavigationController -> Present" to "Push to theOneAndOnlyNavigationControllerInMyApp". The question is: Is there any foreseen way to "close" screen defined as DestinationStep and routed to using DefaultRouter.

To simplify my question there is another example:
I described my screen to be opened as just simple push to some navigationController like that let phaseStep = StepAssembly(finder: finder, factory: factory) .using(UINavigationController.push()) .from(phasesScreen().expectingContainer()) .assemble() and I need to implement a button in PhaseViewController (phaseStep) which just invokes self.navigationController?.pop() (default Back button behavior) using DefaultRouter. How would you implement that in "correct" way. Thanks!

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.

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.

Have different contexts for Factory and navigation

Hello,

After we have replaced most of the navigation logic in our app with the APIs from this library, we have come across, what one might think of, a limitation of the Context approach. Currently, Factory & ContextAccepting protocols both expect a Context type, which, theoretically, can be different, but in practice we realized that it actually has to be the same.

Practically, How can I use a different Context type for creating a ViewController via Factory and a different Context for navigating to such controller and "injecting" a different state?

the Router method navigate accepts a single type of Context, so having 2 different types of Contexts - one for Factory and one for ContextSettingTask is currently not possible.

Usage help: switch-based navigation

I am making this issue to ask for advice, this is not a bug report :)

I'm trying to wrap my head around this library to see if we can use it in production apps. I think it's brilliant but its so different from what I am used to that my brain is a little rusty.

Here's an example app structure I have:

key window ->
    tab bar controller ->
        [tab 1] nav controller -> home controller
        [tab 2] nav controller -> circle controller

I have another controller, the search controller, that I can present from either the home or circle controller. here are my routes:

static let homeTabFactory = CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?> {
        $0.tabBarItem.title = "Home"
        $0.tabBarItem.image = UIImage(systemName: "house")
    })
    .with(ClassFactory<HomeController, Any?>())
    .assemble()

static let circleTabFactory = CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?> {
        $0.tabBarItem.title = "Circle"
        $0.tabBarItem.image = UIImage(systemName: "circle")
    })
    .with(ClassFactory<CircleController, Any?>())
    .assemble()

static let mainTabBarFactory = CompleteFactoryAssembly(factory: TabBarControllerFactory())
    .with(homeTabFactory)
    .with(circleTabFactory)
    .assemble()

static let boot = StepAssembly(
        finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root),
        factory: mainTabBarFactory
    )
    .using(GeneralAction.replaceRoot())
    .from(GeneralStep.root())
    .assemble()

static let home = StepAssembly(
        finder: ClassFinder<HomeController, Any?>(),
        factory: NilFactory() // the home controller is initted at boot and only 1 ever exists
    )
    .from(boot)
    .assemble()

static let search = StepAssembly(
        finder: ClassFinder<SearchController, String?>(),
        factory: ClassFactory<SearchController, String?>()
    )
    .using(UINavigationController.push())
    .from(home.expectingContainer())
    .assemble()

my question involves presenting the SearchController. I have the step search setup to do only part of what I want:

  • if the search controller is visible on screen, just use that and change its context
  • if the user is on the home controller tab, push a new search controller onto its navigation controller
  • if the user is anywhere else in the app, present it modally in a navigation controller

I think I need to use SwitchAssembly to pull it off but I'm unable to write anything that will even compile. I feel like I should also be able to compose with the home step above, but I am coming up blank.

appreciate the help in advance!


🎉 UPDATE:
I think I have written a step that does exactly what I want, however I feel like it might be a bit clunky. In particular, I feel as if I'm cheating when using .expectingContainer() - but maybe that's just because I have not developed a good intuition on when it is truly needed.

This is what I came up with:

static let searchAlt = StepAssembly(
        finder: ClassFinder<SearchController, String?>(options: .currentVisibleOnly, startingPoint: .topmost),
        factory: ClassFactory<SearchController, String?>()
    )
    .using(UINavigationController.push())
    .from(searchNavigationFinder.expectingContainer())
    .assemble()

static let searchNavigationFinder = SwitchAssembly<UINavigationController, Any?>()
    .addCase(
        when: ClassFinder<HomeController, Any?>(options: .currentVisibleOnly, startingPoint: .topmost),
        from: home.expectingContainer()
    )
    .assemble(default: ChainAssembly
        .from(NavigationControllerStep())
        .using(GeneralAction.presentModally(presentationStyle: .automatic))
        .from(GeneralStep.current())
        .assemble()
    )

"Go to product 02" navigation

"Star tab -> Go to product 02" send us to the circle tab.
Is it normal for example?

Also another case:
"Square tab -> Go to Star (logged in) -> Press to Square tab -> Go to split"
Star tab removed. Same question as above.

Help how to close the current screen or pop to previous screen in Route Composer?

help
Can you tell me how to implement a function in Route Composer that simply closes the screen, or I approximately implemented the transition to the previous screen, but it is not correct

var back: DestinationStep<UIViewController, NavigationControllerFactory> {
StepAssembly(
finder: ClassFinder<UIViewController, NavigationControllerFactory.Context>(),
factory: NavigationControllerFactory()
)
.using(UINavigationController.pop())
.assemble()
}

struct NavigationControllerFactory: Factory {

typealias ViewController = UINavigationController
typealias Context = Any?

func build(with context: Any?) throws -> UINavigationController {
    return UINavigationController()
}

}

[Help] Support set root controller direct in AppDelegate

I want to create project, ViewController with xib file ( not use storyboard), embed with UINavigationControlller
Could you support set root controller direct in AppDelegate and support better with xib file (not use storyboard) ?
Example:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { RouteComposerDefaults.configureRootViewController(...) }
You can reference https://github.com/quickbirdstudios/XCoordinator - simple, easy read & use

How to handle routing in a multi-window app?

Hello, I need to figure out the best way to handle multiple windows, and how to properly implement this in my app.

For example, I am implementing a floating window (like a video player) within the app. So the video can be played full screen, and you can minimize it to a small window. The video player view controller may have it's own routes to go to some other screens within the window. While the player is minimized, the user can navigate around the keyWindow of the app, using typical RouteComposer configuration.

It should also support deeplinking so that RouteComposer can automatically create and display the video player in a new window when the app is launched, with the ability to minimize back to the main app.

Is this possible with RouteComposer? I know that you can provide a UIWindow when searching for existing view controllers, but I'm not sure the right way to code this.

Setting title/navigationItem title in a UIViewController in a UITabBarController

Hello, I'm having some trouble figuring out how to set the title and other content inside the navigation bar/title area of a view controller.

I have a UITabBarController and a Factory to build it. I also have a UIViewController with its own Factory. I use CompleteFactoryAssembly to add my controller to the tab bar. When I try to set the title in the Factory for the controller, it doesn't work. It only works if I set the title after the view controller has been loaded (inside viewDidLoad or viewWillAppear). This makes it difficult to set the title inside of the factory.

Is there a way for to access a parent view controller, like a UITabBarController or UINavigationController from inside the factory of a child view controller? For example, I also want to adjust the navigationItems when changing tabs in a tab bar. Some tabs might update the appearance of the navigation bar with content that is relevant to that tab. I think the correct place to do this is when building the view controller in its own Factory. Maybe I am not integrating my view controllers correctly into the tab bar.

Even in the Example project it has a similar problem, SquareViewController has a title "Square" but when you navigate to it, there is no title. CircleViewController is almost identical, but its title displays correctly.

How to push 2 view controllers, both which can accept context

Hello,

In my usecase I have 2 view controllers - View Controller A and View Controller B - which I want to push both on the same navigation stack. Both of the view controllers conform to the ContextAccepting protocol, but I only care of applying a new context for the View Controller B.

Schematically it would look like this:
NavController -> View Controller A -> View Controller B

the routes are defined as follows:

View Controller A

static func viewControllerA() -> DestinationStep<ViewControllerA, ViewControllerA.Context> {
        StepAssembly(
            finder: ClassFinder(),
            factory: ViewControllerAFactory()
        )
        .adding(ContextSettingTask())
        .using(UINavigationController.pushAsRoot())
        .from(NavigationControllerStep<UINavigationController, ViewControllerA.Context>())
        .using(UITabBarController.add())
        .from(tabBar().unsafelyRewrapped())
        .assemble()
    }

View Controller B

static func viewControllerB() -> DestinationStep<ViewControllerB, ViewControllerB.Context> {
        StepAssembly(
            finder: NilFinder(),
            factory: ViewControllerBFactory()
        )
        .using(BPNavigationController.push())
        .from(
            viewControllerA()
                .adaptingContext(
                    using: InlineContextTransformer { (ctx: ViewControllerB.Context) in
                        return ViewControllerA.Context(...)
                    }
                )
                .expectingContainer()
        )
        .assemble()
    }

The "problem" I'm facing is that I don't actually need (or want) to pass any kind of context to View Controller A, but I'm required to by the library.
In my case, the ViewControllerA.Context is not used for building the ViewController, but rather for "injecting" a different state based on the source of the navigation. In this case, though, I would like View Controller A to not change its state if View Controller B is pushed on top of it.

A potential solution might be to make ViewControllerA.Context optional, and in case it is missing, View Controller A could just ignore it, but I was wondering whether the authors of this library envisioned a different approach for solving this.

How should one approach the aforementioned behavior?

Incorrect tag for Swift Package manager

Swift package manger always tries to resolve versions using semantic versioning, so when using the tag 2.5, it automatically tries to resolve the tag as 2.5.0, which doesn't exist, so resolving the package fails.

Tested with Xcode 11.3

Best way to hide a side menu when no action is taken by the Router

I have a Side Menu which is essentially a Container View Controller that has a Menu Controller (never really changes) and a Content Controller. Whenever a menu item is selected, it replaces the main content on the screen, with a new Content View Controller. My issue is when the user opens the side menu, and selects the current menu item, no action is taken by the Router because the Content View Controller is already on screen. This is good, and expected. My question is, where is the best place to hide the menu after the user selects a menu item to navigate to? Is it a PostRouterTask? I need to hide the menu in both these cases:

  1. User selects a new menu item and the router takes an action to replace the Content View Controller
  2. User selects the same menu item on the screen and router takes no action.

I have considered hiding it in

  1. Right before calling router.navigate(to: ...) in my View Controller
  2. Using a PostRouterTask

Modular Architecture

hi @ekazaev, may i have some enlightenment how to implement RouteComposer in Modular Architecture? I ran into a deadlock in unclean code and how to interaction of each module feature 😅

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?

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. 

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:

assertionFailure("Application does not have a key window.")

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

How to display a modal sheet from a tab bar item

How should one present a screen modally from one of the items in the tab bar?
Similar to how LinkedIn has the "post" button in the middle of the tab bar that modally presents a screen for post creation.

I wonder if the authors of this library have already added support for this rather common usecase for tab bars. Normally, this would be implemented via the delegate of the UITabBarController, and specifying delegates is already supported by this library via the TabBarControllerFactory.

Thank you

How get data from dismiss?

My secondVC have StepAssembly with

.adding(DismissalMethodProvidingContextTask(dismissalBlock: { (context, animated, completion) in
            UIViewController.router.commitNavigation(to: GeneralStep.custom(using: PresentingFinder()), with: contextNew, animated: animated, completion: completion)
        }))

how get this context in firstVC?
I am using this as(but i dont like this way):

static func secondDestination(with context: SecondContext, dismissContext: @escaping(CustomDismissContext) -> Void) -> Destination<SecondViewController, SecondContext> {}

into method call dismissContext:

            dismissContext(context) // here call closure
            UIViewController.router.commitNavigation(to: GeneralStep.custom(using: PresentingFinder()), with: contextt, animated: animated, completion: completions)
        }))

Please show true way for that case.

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

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 

Crash in String(describing: dactory) call

Hello Eugene!

We use RouterComposer in our app and encountered the following crash during navigation:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x6a80000000001e) in line # 55 String(describing: factory) of CompleteFactory implementation.

It happens from time and time and it's difficult to reproduce, but I can also see that this call logger?.log(.info("(String(describing: step)) hasn't found a corresponding view " in DefaultRouter precends that.

  • Device: iPhone 14 Pro
  • OS: iOS 16.4
  • RouteComposer version 2.10.4

Will be happy to hear any leads on how to resolve it.

Best way to route reused screen

Hello! I am trying to use your solution and am faced with a misunderstanding how to reuse controller in several navigation chains.

Example1: I have controllers A, B, C, D, E. I want to reuse controller C from A and B.

  1. A or B can present C
  2. C should present D if was presented from A and should present E from B respectively

Example2: I have controllers A, B, C, D. Note: A and B cannot be inherited from AnyContextCheckingViewController: ContextChecking.

  1. A or B can present C
  2. C can present D
  3. If D was presented so we need to dismiss it to source controller A or B. So we need to pass information about source controller through controller C (reused).

I looked for solution for that case in your demo app, but there are all controllers navigate with concrete configuration. How to resolve this situation in correct way? Thank you!

Can a custom init function be added to `NavigationControllerStep` & `TabBarControllerStep`?

I have a CustomNavigationController must be call custom init function, but NavigationControllerStep does not provide custom initialization. Now i have to Now I have to re-instantiate SingleContainerStep to use the custom initialization function

Or you can set the access permission of NavigationControllerStep to open. I can override it.

Do you have any good ideas?

How to implement AuthorizationInterceptor?

I have an authorization's scenario as below:

  1. The NavigationController have LoginViewController as rootViewController.
  2. In the LoginViewController, the user taps on the SignUp button, the system navigates to SignUpViewController.
  3. In the SignUpViewController, the user fills their's information, then he taps on the register button, if success, the system navigates to HomeViewController.

Expectation:
HomeViewController should be pushed/presented or setAsRoot of the window after authorized.

@ekazaev Could you provide an example of this?

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.

Is it easy to use for large SwiftUI-only project now?

Hello!

Just found your library. Great job! It is really useful for large UIKit projects.

I am curious if it is as useful for SwiftUI projects?

Learned the example project and found out that I have to wrap all my views into UIHostingController in case to make navigation, which is sad ;(

I am starting a new big SwiftUI B2C Project, and I really liked this library.
However, I was not happy with the usage as in the example, because I was confused with some things, such as:

  • All SwiftUI Views have to be wrapped into UIHostingControllerWithContextFinder()
  • Did not get it how to use it with TabBar navigation.
  • Also, tbh, I worry, that if no one did not check RouteComposer in large SUI Projects and I will be first, I might get some unexpected bugs.

Please correct me if I am wrong (I hope so).

I am really looking forward to know your opinion about using RouteComposer in big SwiftUI Projects and get help for my questions.

I really want to use it, because I am tired of using awkward tools for routing and navigating, such as coordinators, that do not support deep linking, or even worse, native navigation (especially in SwiftUI).

Thank you for your answer! Looking forward to it ;)

Use .from(OtherDestinationStep) only with same context?

I want to use a view controllers chain in one navigation controller. I have to use the same context for each view controller on the stack. How to be in this case? Use type Any for context and later convert to the desired type?

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?

Custom navigation using setViewControllers(_:animated:)

Есть UINavigationController, который содержит 5 контроллеров: 1->2->3->4->5.
Я хочу добавить еще один после 2, т.е. должно получится так: 1->2->6.

В текущей реализации сначала происходит сброс до 2 контроллера, а потом переход на 6, что выглядит не очень красиво.

Подскажите, пожалуйста, как можно это решить?

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.