5

I have an app using MVP with the Coordinator pattern.

When a child coordinator sends and event, I would expect my AppCoordinator to recursively call a method that selects the next coordinator based on some SessionState.

The basic flow of the app is as follows -

AppCoordinator

  1. start() invokes coordinateToRoot with an initial state
  2. Subscribes to showStartScene() which starts the child coordinator

StartCoordinator

  1. start() creates MVP module which is now visible to the user
  2. MVP module invokes AuthSvc which makes async call to iDP and confirms auth state
  3. On completion of this task, publishes an event which is picked up by the subscription in the AppCoordinator's coordinateToRoot method and the cycle repeats using the appropriate coordinator for the view state.

The issue however is that on the publish of that event, nothing is happening. start() is not showing it received the event and coordinateToRoot is not called again.

I have created the most basic version I can below to demonstrate this. I have also hardcoded showStartScene to return .signedIn rather than a look up of the auth state.

In the below example, I would expect once the view is loaded, presenter.signal should immediately emit an event that causes a print statement to show.

SessionState

enum SessionState: String {
    case unknown, signedIn, signedOut
}

AppCoordinator

final class AppCoordinator: BaseCoordinator<Void> {

    private let window: UIWindow

    init(window: UIWindow) {
        self.window = window
    }

    override func start() -> Observable<Void> {
        coordinateToRoot(basedOn: .unknown)
        return .never()
    }

    /// Recursive method that will restart a child coordinator after completion.
    /// Based on:
    /// https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/issues/3
    private func coordinateToRoot(basedOn state: SessionState) {

        switch state {
        case .unknown:
            return showStartScene()
                .subscribe(onNext: { [unowned self] state in
                    self.window.rootViewController = nil
                    self.coordinateToRoot(basedOn: state)
                })
                .disposed(by: disposeBag)

        case .signedIn:
            print("I am signed in")

        case .signedOut:
            print("I am signed out")
        }
    }

    private func showStartScene() -> Observable<SessionState> {
        let coordinator = StartCoordinator(window: window)
        return coordinate(to: coordinator).map { return .signedIn }
    }
}

StartCoordinator

final class StartCoordinator: BaseCoordinator<Void> {

    private(set) var window: UIWindow

    init(window: UIWindow) {
        self.window = window
    }

    override func start() -> Observable<CoordinationResult> {

        let viewController = StartViewController()
        let presenter = StartPresenter(view: viewController)

        viewController.configurePresenter(as: presenter)

        window.rootViewController = viewController
        window.makeKeyAndVisible()

        return presenter.signal
    }
}

Start MVP Module

protocol StartViewInterface: class {
    func configurePresenter(as presenter: StartPresentation)
}

protocol StartPresentation: class {
    var viewIsReady: PublishSubject<Void> { get }
    var signal: PublishSubject<Void> { get }
}
// MARK:- StartPresenter
final class StartPresenter {

    // Input
    let viewIsReady = PublishSubject<Void>()

    // Output
    let signal = PublishSubject<Void>()

    weak private var view: StartViewInterface?

    private lazy var disposeBag = DisposeBag()

    init(view: StartViewInterface?) {
        self.view = view

        viewIsReady.bind(to: signal).disposed(by: disposeBag)
    }

}

extension StartPresenter: StartPresentation { }

// MARK:- StartViewController
final class StartViewController: UIViewController {

    private var presenter: StartPresentation?

    override func viewDidLoad() {
        super.viewDidLoad()

        if let presenter = presenter {
            presenter.viewIsReady.onNext(())
        }

    }
}

extension StartViewController: StartViewInterface {
    func configurePresenter(as presenter: StartPresentation) {
        self.presenter = presenter
    }
}


Interestingly if I do something like this in StartCoordinator the process does work, it has however not what I am trying to achieve.

    override func start() -> Observable<CoordinationResult> {

        let viewController = StartViewController()
        let presenter = StartPresenter(view: viewController)

        viewController.configurePresenter(as: presenter)

        window.rootViewController = viewController
        window.makeKeyAndVisible()


        let subject = PublishSubject<Void>()


        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            subject.onNext(())
        }

        return subject
    }

For reference my BaseCoordinator looks like -

/// Base abstract coordinator generic over the return type of the `start` method.
class BaseCoordinator<ResultType>: CoordinatorType {

    /// Typealias which allows to access a ResultType of the Coordainator by `CoordinatorName.CoordinationResult`.
    typealias CoordinationResult = ResultType

    /// Utility `DisposeBag` used by the subclasses.
    let disposeBag = DisposeBag()

    /// Unique identifier.
    internal let identifier = UUID()

    /// 1. Stores coordinator in a dictionary of child coordinators.
     /// 2. Calls method `start()` on that coordinator.
     /// 3. On the `onNext:` of returning observable of method `start()` removes coordinator from the dictionary.
     ///
     /// - Parameter coordinator: Coordinator to start.
     /// - Returns: Result of `start()` method.
     func coordinate<T: CoordinatorType, U>(to coordinator: T) -> Observable<U> where U == T.CoordinationResult {
         store(coordinator: coordinator)
         return coordinator.start()
             .do(onNext: { [weak self] _ in self?.free(coordinator: coordinator) })
     }

     /// Starts job of the coordinator.
     ///
     /// - Returns: Result of coordinator job.
     func start() -> Observable<ResultType> {
         fatalError(message: "Start method should be implemented.")
     }

    /// Dictionary of the child coordinators. Every child coordinator should be added
    /// to that dictionary in order to keep it in memory.
    /// Key is an `identifier` of the child coordinator and value is the coordinator itself.
    /// Value type is `Any` because Swift doesn't allow to store generic types in the array.
    private(set) var childCoordinators: [UUID: Any] = [:]

    /// Stores coordinator to the `childCoordinators` dictionary.
    ///
    /// - Parameter coordinator: Child coordinator to store.
    private func store<T: CoordinatorType>(coordinator: T) {
        childCoordinators[coordinator.identifier] = coordinator
    }

    /// Release coordinator from the `childCoordinators` dictionary.
    ///
    /// - Parameter coordinator: Coordinator to release.
    private func free<T: CoordinatorType>(coordinator: T) {
        childCoordinators[coordinator.identifier] = nil
    }
}

EDIT I added some debug operators and I can see the order appears off for the next event and the subscription

2019-11-08 10:26:19.289: StartPresenter -> subscribed
2019-11-08 10:26:19.340: StartPresenter -> Event next(())
2019-11-08 10:26:19.350: coordinateToRoot -> subscribed

Why is coordinateToRoot subscribing after StartPresenter is created?

Tim J
  • 1,211
  • 1
  • 14
  • 31
  • I believe `coordinateToRoot` subscribes *after* start is called on the child coordinator due to the `coordinate` method on your `BaseCoordinator` class. The subscription takes place after the view has been setup. – nodediggity Nov 08 '19 at 10:35

1 Answers1

0

coordinateToRoot is not connected to the lifecycle of the Observable returned by AppCoordinator.start(_:). This means that there is no guarantee to the order in which coordinateToRoot and StartPresenter are subscribed to.

To guarantee the order, I think you can use the do operator and pass a closure for the onSubscribe argument. This onSubscribe closure will run before the subscribing to the underlying observable.

Here is the change I think you could make:

final class AppCoordinator: BaseCoordinator<Void> {

    override func start() -> Observable<Void> {
        return Observable<Void>.never().do(onSubscribe: { [weak self] _ in
             self?.coordinateToRoot(basedOn: .unknown)
        })
    }
}