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
start() invokes coordinateToRoot with an initial state
Subscribes to showStartScene() which starts the child coordinator
StartCoordinator
start() creates MVP module which is now visible to the user
MVP module invokes AuthSvc which makes async call to iDP and confirms auth state
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?
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)
})
}
}
Related
I got a problem. I got a collectionview which is binded to a winPinataActions PublishSubject<[Object]>(). Initially, when loading collectionview everything is fine, it displays as it has to the objects, however when the pull to refresh action changes the publishSubject data the UI is not updated, it still gets the old content of the PublishSubject.
Here is how I bind the collectionView :
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
}
func configureCollectionView() {
/..../
viewModel.winPinataActions
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}.disposed(by: bag)
viewModel.getPinataActions()
}
#objc func handleRefreshControl() {
viewModel.getPinataActions()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.collectionView.refreshControl?.endRefreshing()
}
}
}
This is my viewModel class:
class WinPinatasViewModel {
let winPinataActions = PublishSubject<[WinPinatasAction]>()
func getPinataActions() {
guard let ssoId = UserDefaultsStore.ssoId() else {
return
}
NetworkEngine.shared.gamificationNetwork.getUserWinPinataActions(subject: winPinataActions, ssoID: ssoId)
}
}
And my NetworkEngine getuserPinataActions method:
func getUserWinPinataActions(subject winPinatasActions: PublishSubject<[WinPinatasAction]>, ssoID: String) {
//...//
let actions = try decoder.decode([WinPinatasAction].self, from: jsonData)
winPinatasActions.onNext(actions)
winPinatasActions.onCompleted()
//...//
}
When the pull to refresh action is done, the handleRefreshControl() method is called. Also While debugging I could see that after pullToRefresh action the new data is received inside my NetworkEngine method and both .onNext()and onCompleted() are called. But when I scroll through the collectionView the data the cell items are from the old array, not the one new one. Could you help me please? What am I doing wrong?
The problem here is that you are sending a completed event to the Subject but then expecting it to be able to send other events after that. The Observable contract specifies that once an Observable (or Subject in this case) sends a completed event, it will never send any more events under any circumstances.
Instead of passing a Subject into getUserWinPinataActions you should be returning an Observable from the function.
This is closer to what you should have:
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
let viewModel = WinPinatasViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.refreshControl!.rx.controlEvent(.valueChanged)
.startWith(())
.flatMapLatest { [viewModel] in
viewModel.getPinataActions()
}
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}
.disposed(by: bag)
}
}
class WinPinatasViewModel {
func getPinataActions() -> Observable<[WinPinatasAction]> {
guard let ssoId = UserDefaultsStore.ssoId() else {
return .empty()
}
return GamificationNetwork.shared.getUserWinPinataActions(ssoID: ssoId)
}
}
class GamificationNetwork {
static let shared = GamificationNetwork()
func getUserWinPinataActions(ssoID: String) -> Observable<[WinPinatasAction]> {
Observable.create { observer in
let jsonData = Data() // get jsonData somehow
let actions = try! decoder.decode([WinPinatasAction].self, from: jsonData)
observer.onNext(actions)
observer.onCompleted()
return Disposables.create { /* cancelation code, if any */ }
}
}
}
Remember:
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use... In production code you may find that you rarely use the IObserver interface and subject types... The IObservable interface is the dominant type that you will be exposed to for representing a sequence of data in motion, and therefore will comprise the core concern for most of your work with Rx...
-- Intro to Rx
If you find yourself reaching for a Subject to solve a problem, you are probably doing something wrong.
Also, this article might help: Integrating RxSwift Into Your Brain and Code Base
I'm trying to test my coordinator flow but the child coordinator deinit called before the unit test case finished
My coordinator class
public final class AppCoordinator: Coordinator {
public var childCoordinators: [Coordinator] = []
public var navigationController: UINavigationController
var window: UIWindow?
public init(window: UIWindow?) {
self.window = window
let secController = SecController()
self.navigationController = UINavigationController(rootViewController: secController)
secController.delegate = self
}
public func start() {
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
}
extension AppCoordinator: SecControllerDelegate, SignInControllerDelegate {
public func removeSingIn() {
self.childCoordinators.removeFirst()
}
public func showSignIn() {
let signInCoordinator = SignInCoordinator(navigationController: self.navigationController)
signInCoordinator.delegate = self
self.childCoordinators.append(signInCoordinator)
signInCoordinator.start()
}
}
Unit test class
class AppCoordinatorTests: XCTestCase {
var coordinator: AppCoordinator!
override func setUp() {
super.setUp()
coordinator = AppCoordinator(window: UIWindow())
}
override func tearDown() {
coordinator = nil
super.tearDown()
}
func testStartMethod() {
coordinator.start()
XCTAssertNotNil(coordinator.window?.rootViewController)
}
func testShowSignIn() {
coordinator.showSignIn()
XCTAssertFalse(coordinator.childCoordinators.isEmpty)
XCTAssertTrue(coordinator.navigationController.visibleViewController is SignInController)
}
}
when try to test testShowSignIn always failed because of the deinit call removeSingIn function
public class SignInController: UIViewController {
public weak var delegate: SignInControllerDelegate?
public init() {
super.init(nibName: nil, bundle: nil)
}
deinit {
self.delegate?.removeSingIn()
}
}
Let's review the steps:
testShowSignIn calls coordinator.showSignIn(), where coordinator is an AppCoordinator.
showSignIn() instantiates a SignInCoordinator, and sets its delegate to the AppCoordinator instance.
Now we reach the important part:
We reach the end of showSignIn(). The SignInCoordinator goes out of scope, so Swift destroys it.
Nothing maintains a reference to the SignInCoordinator. But you want to test the interaction between the AppCoordinator and the SignInCoordinator. The code is fighting you, because AppCoordinator decides to create and destroy the SignInCoordinator on its own.
You can test it by changing the design. You have a couple of options.
Option 1: Change AppCoordinator to have a lazy computed property that returns the SignInCoordinator. This can work if you're okay with that design. Then the SignInCoordinator will continue to live, so that the test can query it. This improves the testability of AppCoordinator by exposing the SignInCoordinator.
Option 2: Have the test create a SignInCoordinator and pass it in as an argument to showSignIn(). Then the SignInCoordinator lifecycle will be managed completely outside of AppCoordinator.
I am working on a project based on the following app:
MVVMC-SplitViewController
I am trying to write a unit test around the BaseCoordinator class.
I would like to assert that this method within the class
private func free<T: CoordinatorType>(coordinator: T) {
childCoordinators[coordinator.identifier] = nil
}
does in fact free the coordinator from the childCoordinators dictionary.
I am unsure how I can do this though. I thought I could simply create a mock coordinator and have the start method return something, but I believe I am doing this wrong
My Test
func test_releases_coordinator_from_child_coordinator_dict() {
class MockCoordinator: BaseCoordinator<Void> {
override func start() -> Observable<Void> {
return .empty()
}
}
let mockCoordinator = MockCoordinator()
let sut = BaseCoordinator<Void>()
sut.coordinate(to: mockCoordinator).subscribe().disposed(by: disposeBag)
XCTAssertEqual(sut.childCoordinators.count, 0)
}
My base coordinator
import RxSwift
/// 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()
/// 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
}
/// 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("Start method should be implemented.")
}
}
You cannot use .empty as your return type in MockCoordinator.
empty creates an Observable that emits no items but terminates without fail.
You should update your mock to emit a value once subscribed too, eg:
class MockCoordinator: BaseCoordinator<Bool> {
override func start() -> Observable<Bool> {
return .of(true)
}
}
This should invoke the call to free your coordinator.
I am trying to learn binding and understand the MVVM approach in Swift.
I was expecting the below example to work, essentially someEventHappened is called, this invokes the onEvent closure and my message is logged to the screen.
This does not happen however, nothing is printed and I am a little unsure as to why?
class ViewModal {
public var onEvent: (() -> Void)?
func someEventHappened() -> Void {
onEvent?()
}
}
class ViewController: UIViewController {
lazy var viewModel: ViewModal = {
let viewModal = ViewModal()
return viewModal
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .purple
viewModel.someEventHappened()
viewModel.onEvent = {
print("something happened")
}
}
}
Just swap assigning onEvent and calling someEventHappened
viewModel.onEvent = {
print("something happened")
}
viewModel.someEventHappened()
this is because you're calling onEvent handler inside someEventHappened and in viewDidLoad you first had called someEventHappened and then assigned onEvent
Everytime we resolve a protocol/class then we get a new object. My coordinator needs a view model and my controller needs the same view model.
internal class LoginFactory: Assembly {
func assemble(container: Container) {
let delegate = UIApplication.shared.delegate as? AppDelegate
container.register(LSViewModel.self) { res in
return LSViewModel()
}
container.register(LSCoordinator.self, factory: { res in
let cord = LSCoordinator(window: (delegate?.window)!)
cord.viewModel = res.resolve(LSViewModel.self)
return cord
})
container.register(LSViewController.self) { res in
let cont = StoryboardScene.Login.lsViewController.instantiate()
cont.viewModel = res.resolve(LSCoordinator.self)?.viewModel
return cont
}
}
}
The coordinator goes like
internal final class LSCoordinator: BaseCoordinator<Void>, Coordinator, HasDisposeBag {
private let window: UIWindow
weak var navigationController: UINavigationController!
//LSViewModel implements LSViewModelType
var viewModel: LSViewModelType!
init(window: UIWindow) {
self.window = window
}
func start() -> Observable<Void> {
let lslViewController: LSViewController = DependencyManager.getResolver().resolve(LSViewController.self)!
navigationController = UINavigationController(rootViewController: lsController)
viewModel.outputs.doneTapped
.subscribe(onNext: { [weak self] in self?.showLoginTypes() }).disposed(by: disposeBag)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return .never()
}
func showLoginTypes() {
print(“blah blah”)
}
}
The problem is, when I am trying to inject viewModel in my lsViewController then a different instance of lsViewModel is created. As a result the Rx bindings are not working and the print statement is not executed. Is there any way to pass the same view model to both coordinator and controller?