ekazaev / route-composer

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.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

0rtm opened this issue · comments

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. 

@0rtm Thank you. I do not really know how to solve that issue. What you are facing is tha lack of transactional support in UIKit :( My best suggestion here is to use a delay. AFAIK people use it for such complicated cases without route composer. I cant come with the better solution now, as apple does not provide the way to find out how long pop/push animation takes and UINavigationController is not consistent in its behaviour.

    static var accountHome: DestinationStep <TabbarViewController, NavigationContext?> {
        return StepAssembly(
            finder: ClassWithContextFinder<TabbarViewController, NavigationContext?>(),
            factory: TabBarFactory())
            .adding(ContextSettingTask())
            .using(DelayedContainerAction(UINavigationController.push(), delay: 500))
            .from(accountSelector.expectingContainer())
            .assemble()
    }

/// ...

public struct DelayedContainerAction<A: ContainerAction>: ContainerAction {
    
    // MARK: Associated types
    
    /// Type of the `UIViewController` that `Action` can start from.
    public typealias ViewController = A.ViewController
    
    // MARK: Properties
    
    let action: A
    
    let milliseconds: Int
    // MARK: Methods
    
    public init(_ action: A, delay milliseconds: Int) {
        self.action = action
        self.milliseconds = milliseconds
    }
    
    public func perform(embedding viewController: UIViewController, in childViewControllers: inout [UIViewController]) throws {
        try action.perform(embedding: viewController, in: &childViewControllers)
    }
    
    public func perform(with viewController: UIViewController, on existingController: A.ViewController, animated: Bool, completion: @escaping (RoutingResult) -> Void) {
        guard animated else {
            action.perform(with: viewController, on: existingController, animated: false, completion: completion)
            return
        }
        var actionResult: RoutingResult = .failure(RoutingError.compositionFailed(.init("Wrapped \(action) did not complete correctly.")))
        self.action.perform(with: viewController, on: existingController, animated: true, completion: { result in
            actionResult = result
        })
        let deadline = DispatchTime.now() + .milliseconds(milliseconds)
        DispatchQueue.main.asyncAfter(deadline: deadline) {
            completion(actionResult)
        }
    }
    
}

If you can suggest me a better way to do it i am open.

Basiaclly what router does:

// step 1
modalAccountViewController.dismiss(animted: true, completion: {
  // step 2
  navigationController.popToViewController(accountSelectorViewController)
  // step 3
  navigationController.push(redViewController)
  // Here Router expects navigationController.viewControllers to have 2 view controllers inside even though the animation is not finished, which is normal behaviout of UINavigationController but it is not what we see in the reality.  So the only solution is to surround it with the delay
  // step 4
  redViewController.presentViewController(modalAccountViewController.dismiss, animate: true. completion: { 
   // success
})
})

Your second option not to use any tricks but update the configuration to avoid the UIKit issue. It will give you a sligtly different animation but the steps will avoid pop/push animation sequence:

    static var accountHome: DestinationStep <TabbarViewController, NavigationContext?> {
        return SwitchAssembly<TabbarViewController, NavigationContext?>()
            .addCase(when: ClassFinder<TabbarViewController, NavigationContext?>(), from: StepAssembly(
                finder: ClassWithContextFinder<TabbarViewController, NavigationContext?>(),
                factory: TabBarFactory())
                .adding(ContextSettingTask())
                .using(UINavigationController.pushReplacingLast())
                .from(GeneralStep.custom(using: ClassFinder<TabbarViewController, NavigationContext?>(options: .fullStack)).expectingContainer())
                .assemble())
            .assemble(default: {
                return StepAssembly(
                    finder: ClassWithContextFinder<TabbarViewController, NavigationContext?>(),
                    factory: TabBarFactory())
                    .adding(ContextSettingTask())
                    .using(UINavigationController.push())
                    .from(accountSelector.expectingContainer())
                    .assemble()
            })
    }

@0rtm Please let me know your opinion on the suggested solutions

@0rtm Please let me know your opinion on the suggested solutions

Second case looks better since it does rely on magical time values. However, it prints that routing finished unsuccessfully, producing this 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] FinderStep(finder: Optional(ClassFinder<TabbarViewController, Optional<NavigationContext>>(iterator: RouteComposer.DefaultStackIterator(options: current, contained, presented, presenting, startingPoint: RouteComposer.DefaultStackIterator.StartingPoint.topmost, windowProvider: RouteComposer.KeyWindowProvider(), containerAdapterLocator: RouteComposer.DefaultContainerAdapterLocator())))) found <routeComposerBug.TabbarViewController: 0x7fc72e001200> to start the navigation process from.

[Router] Started to build the view controllers stack.

[Router] TabBarFactory() built a <routeComposerBug.TabbarViewController: 0x7fc72d834000>.

[Router] PushReplacingLastAction<UINavigationController>() has applied to <routeComposerBug.TabbarViewController: 0x7fc72e001200> with <routeComposerBug.TabbarViewController: 0x7fc72d834000>.

[Router] 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()))) built a <routeComposerBug.RedViewController: 0x7fc72d51ea70>.

[Router] NilAction() has applied to <routeComposerBug.TabbarViewController: 0x7fc72d834000> with <routeComposerBug.RedViewController: 0x7fc72d51ea70>.

[Router] ClassFactory<ModalVCViewController, Optional<NavigationContext>>(nibName: nil, bundle: nil, configuration: nil) built a <routeComposerBug.ModalVCViewController: 0x7fc72d720c00>.

[Router] CATransactionWrappedAction<PresentModallyAction>(action: RouteComposer.ViewControllerActions.PresentModallyAction(presentationStyle: Optional(__C.UIModalPresentationStyle), transitionStyle: Optional(__C.UIModalTransitionStyle), preferredContentSize: nil, popoverControllerConfigurationBlock: nil, transitioningDelegate: nil)) has stopped the navigation process as it was not able to build a view controller into a stack.

[Router] Composition Failed Error: Wrapped PresentModallyAction(presentationStyle: Optional(__C.UIModalPresentationStyle), transitionStyle: Optional(__C.UIModalTransitionStyle), preferredContentSize: nil, popoverControllerConfigurationBlock: nil, transitioningDelegate: nil) did not complete correctly.

[Router] Unsuccessfully finished the navigation process.

@0rtm Remove all the CATransaction wrappers please. It should wrap only actions that support them properly. Not the modal presentation for instance. Second configuration works fine for me.

@0rtm Remove all the CATransaction wrappers please. It should wrap only actions that support them properly. Not the modal presentation for instance. Second configuration works fine for me.

Indeed it does work without transactions. Thanks

@0rtm You are welcome. Ill mark this issue as wont fix as there is no proper ways to make pop and push animation without side effects. But ill keep it in mind for the future improvements if i can make Router to be wise enough and rewrite the configuration automatically.
Thank you for help

@0rtm I did some investigations today, and it seems that there is no straightforward way to achieve expected behaviour because of the UIKit limitations (no competition methods basically). The Router knows how to merge actions together to avoid such situations when it builds the view controller stack itself, but it can not be achieved with making the view controller visible in the existing stack because the different containers make view controllers visible in the different ways (UINavigationController actually modifies stack, but UITabBarController for example just switches one view controller to another and so on). But expected behaviour can be achieved by making intermediate view controllers visible without animation. And i think i will update the router this way, so complicated configurations like yours will execute correctly and wont fail. If the developer wants a different behaviour, he can write more complicated configuration like i gave you in the example that achieve that.
I think this is a right way to do it so the correct configuration like yours wont fail.