iosswiftrx-swiftrx-cocoa

RxSwift concurrency problems with coordinator


What I want to do:

  1. Present VC1
  2. When VC1 is dismissed, present VC2

Problem:

  1. When VC1 is dismissed, VC2 does not present

Dirty Fix: Put milisecond delay. It fixes the problem, but want to know why it happens

Explanation: I get viewDidDissapear event when VC1 dismisses so I can present VC2

If you need more details, please ask.

Code:

class ViewModel {

    let coordinator = Coordinator()

    struct Input {
        let itemSelected: Driver<IndexPath>
    }

    struct Output {
        let presentVC1: Driver<Void>
        let presentVC2: Driver<Void>
    }

    func transform(input: Input) -> Output {

        let navigateToVC1 = input.itemSelected
            .flatMap { [coordinator] in
                return coordinator.transition(to: Scene.VC1)
            }

        let navigateToVC2 = navigateToVC1
            .delay(.milliseconds(1))
            .flatMap { [coordinator] in
                return coordinator.transition(to: Scene.VC2)
            }

        return Output(presentVC1: presentVC1, presentVC2: presentVC2)
    }

Coordinator code:

func transition(to scene: TargetScene) -> Driver<Void> {
        let subject = PublishSubject<Void>()

        switch scene.transition {
           
            case let .present(viewController):
                _ = viewController.rx
                    .sentMessage(#selector(UIViewController.viewDidDisappear(_:)))
                    .map { _ in } 
                    .bind(to:subject)
                currentViewController.present(viewController, animated: true)
                
        return subject
            .take(1)
            .asDriverOnErrorJustComplete()
    }

Solution

  • The viewDidDisappear method is called before the view controller is fully dismissed. You should not try to present the second view controller until the callback of dismiss is called.

    Wherever you are dismissing your view controller, use the below instead and don't present the next view controller until after the observable emits a next event.

    extension Reactive where Base: UIViewController {
        func dismiss(animated: Bool) -> Observable<Void> {
            Observable.create { [base] observer in
                base.dismiss(animated: animated) {
                    observer.onNext(())
                    observer.onCompleted()
                }
                return Disposables.create()
            }
        }
    }
    

    I suggest you consider using my Cause-Logic-Effect architecture which contains everything you need to properly handle view controller presentation and dismissal.

    https://github.com/danielt1263/CLE-Architecture-Tools

    A portion of the interface is below:

    /**
    Presents a scene onto the top view controller of the presentation stack. The scene will be dismissed when either the action observable completes/errors or is disposed.
    - Parameters:
    - animated: Pass `true` to animate the presentation; otherwise, pass `false`.
    - sourceView: If the scene will be presented in a popover controller, this is the view that will serve as the focus.
    - scene: A factory function for creating the Scene.
    - Returns: The Scene's output action `Observable`.
    */
    func presentScene<Action>(animated: Bool, overSourceView sourceView: UIView? = nil, scene: @escaping () -> Scene<Action>) -> Observable<Action>
    
    extension NSObjectProtocol where Self : UIViewController {
    
        /**
            Create a scene from an already existing view controller.
        
            - Parameter connect: A function describing how the view controller should be connected and returning an Observable that emits any data the scene needs to communicate to its parent.
            - Returns: A Scene containing the view controller and return value of the connect function.
        
            Example:
        
            `let exampleScene = ExampleViewController().scene { $0.connect() }`
            */
        func scene<Action>(_ connect: (Self) -> Observable<Action>) -> Scene<Action>
    }
    
    struct Scene<Action> {
        let controller: UIViewController
        let action: Observable<Action>
    }
    

    The connect function is your view model, when its Observable completes or its subscriber disposes, the view controller will automatically dismiss.

    The presentScene function is your coordinator. It handles the actual presenting and dismissing of scenes. When you dismiss and present a new scene, it will properly handle waiting until the previous view controller is dismissed before presenting the next one.