iOS Swift Coordinator pattern and back button of Navigation Controller
Asked Answered
S

5

6

I am using pattern MVVM+Coordinator. Every my controllers are created by coordinators. But what is the correct way to stop my coordinators when tapping on back button of Navigation Controller?

class InStoreMainCoordinator: NavigationCoordinatorType, HasDisposeBag {

    let container: Container

    enum InStoreMainChildCoordinator: String {
        case menu = "Menu"
        case locations = "Locations"
    }

    var navigationController: UINavigationController
    var childCoordinators = [String: CoordinatorType]()

    init(navigationController: UINavigationController, container: Container) {
        self.navigationController = navigationController
        self.container = container
    }

    func start() {
        let inStoreMainViewModel = InStoreMainViewModel()
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel

        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}
Saidel answered 12/1, 2019 at 2:50 Comment(1)
Let's face it: In UIKit, the "Coordinator Pattern" looks good on paper, but it is very, very, difficult to implement correctly. Let's start with your statement: "Every my controllers are created by coordinators". This effectively yields an implementation which is never without issues, that ultimately yields to edge case where your app terminates due to "fatal errors", or behaves incorrectly;)Kennykeno
C
3

What I do now, after reading many articles about Coordinators and seeing some complex ideas like Routers, some swizzling magic and custom Navigation Controller delegates is:

View Controller strongly owns Coordinator, and Coordinator has weak reference to View Controller if at all. Coordinator has weak reference to his parent, to support Chain of Responsibility for communication between Coordinator objects.

(Example of Chain of Responsibility design pattern would be Responder Chain in iOS.)

The moment you call stop on some coordinator, view controller pops from stack, deallocates and frees coordinator. So when back button is tapped, and view controller dismissed, the coordinator is deallocated.

This works for me as there is no need to build additional infrastructure.

Initially I have solved UINavigationControllerDelegate issue by building NavigationControllerMutliDelegate class which conformed to UINavigationControllerDelegate protocol. It had register/unregister logic. Then this object was passed to every Coordinator to notify coordinator when view controller dismisses. NavigationControllerMutliDelegate was an example of a Visitor design pattern, it had bunch of coordinators and on View Controller appear/dismiss it notified all coordinators by sending an object to each.

But, in the end, when seeing how much code there is and unneeded complexity, I just went with View Controller owning Coordinator. I just want object to be above View Controller that keeps data providers, services, view models and whatever so that View Controller is cleaner. I do not want to reinvent push pop stack of coordinators and deal with so much owner problems. Like I want something to ease my life not complicate it more..

Cnidoblast answered 3/11, 2020 at 14:36 Comment(1)
Indeed, while implementing your own custom "solution" seems to achieve your "ideal" goals, you are also deviating from the native frameworks that Apple provides. The more you deviate from the system framework the more boilerplate code you have to write and maintain.Senhorita
U
1

My approach is to use a root (parent) coordinator that manage child coordinators, so when the user finish a flow or tap on back button a delegate method in root coordinator is called and it can clean the child coordinator and create a new one if needed.

Uigur answered 12/1, 2019 at 7:18 Comment(0)
S
1

Coordinator pattern has a known blind spot regarding the native back button. You mainly have two ways to fix it:

  • Reimplement your own back button, although you loose the native swipe back gesture to navigate back.
  • Implement UINavigationControllerDelegate to detect when a view has popped to be able to deallocate the matching Coordinator.

Regarding the first solution, I don't suggest this one, user would pay the price for your code architecture, it doesn't sound fair.

For the second one, you can implement it to the Coordinator itself as suggested by @mosbah, but I would suggest you go further and separate the Navigation to the Coordinator by using a NavigationController or Router class to isolate the navigation itself and keep a clear separation of concern.

I wrote something about it here that details the main steps.

Sikh answered 5/4, 2019 at 3:16 Comment(1)
There is a third solution. Use a function instead of a class to represent your coordinator. Then you have no ownership issues to worry about.Zebadiah
Z
1

My solution is to use a function as my coordinator instead of a class. That way I have no ownership issues at all. When the back button is hit, the views from the view controller emit completed events and everything just naturally unwinds with no effort on my part.

The start() you show in your example can be expressed much more simply by just:

func startInStore(navigationController: UINavigationController) {
    let inStoreMainViewModel = InStoreMainViewModel()
    let inStoreMainController = InStoreMainController()
    inStoreMainController.viewModel = inStoreMainViewModel

    navigationController.pushViewController(inStoreMainController, animated: true)
}

A sample app using this style can be found here: https://github.com/danielt1263/RxMyCoordinator

Zebadiah answered 4/11, 2020 at 13:18 Comment(0)
A
1

Instead of using child coordinators you could write your coordinator classes in such way that they do not need to be retained at all. In fact in the example you gave there is nothing that makes this class needed to be retained and you could even minimize it to a following form:

class InStoreMainCoordinator {
    func start(with navigationController: UINavigationController, container: Container) {
        let inStoreMainViewModel = InStoreMainViewModel()
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel
        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}

Then just call InStoreMainCoordinator().start(with: navigationController, container: container) when you want to launch this screen. You do not need to keep a strong reference to this InStoreMainCoordinator at all. This way you do not have any problem with back button as you do not need to deallocate those coordinators. They exist only when you are switching the screen to a new one. To get a better understanding of this method let's say that you have another screen represented by for example InStoreDetailsController class and that details screen should be launched after clicking something on InStoreMainController. Then you could implement two coordinator classes related to those view controllers like this:

class InStoreMainCoordinator {
    func start(with navigationController: UINavigationController, container: Container) {
        let inStoreMainViewModel = InStoreMainViewModel(onStoreSelected: { storeId in
            InStoreDetailsCoordinator().start(with: navigationController, container: container, dependencies: .init(storeId: storeId))
        })
        let inStoreMainController = InStoreMainController()
        inStoreMainController.viewModel = inStoreMainViewModel
        navigationController.pushViewController(inStoreMainController, animated: true)
    }
}
class InStoreDetailsCoordinator {
    struct Dependencies {
        var storeId: String
    }
    func start(with navigationController: UINavigationController, container: Container, dependencies: Dependencies) {
        let inStoreDetailsViewModel = InStoreDetailsViewModel(storeId: dependencies.storeId)
        let inStoreDetailsController = InStoreDetailsController()
        inStoreDetailsController.viewModel = inStoreDetailsViewModel
        navigationController.pushViewController(inStoreDetailsController, animated: true)
    }
}

As you can see if you use closures instead of delegate pattern you can write everything related to one screen in a single function (including pushing it on the screen and handling events related for example to moving from this screen to a different screen). This way you can have only a single method per screen in your coordinators that you call when you need to switch the screen and you do not need to retain them as everything needed to be retained is retained by something else (in above example view model retains the handler given in onStoreSelected parameter which is used to switch to another screen). I think this solution is simpler than using child coordinators. It works fine and it does not require any additional special handling for back button.

Another alternative solution which works perfectly fine especially if you do not have a very large amount of screens in your app is to create startNameOfYourScreen(...) methods for each view controller in your main AppCoordinator class or whatever you named. As you can see above if you use closures instead of delegate pattern you can write everything related to one screen in a single function which allows to keep it quite simple. You could optionally split those functions into extensions of AppCoordinator class and put them into separate files to have a bit better organization in you project or for example split them to different classes based on the tab in which they appear (for apps with tab bar). In this solution you also do not have any problem with back button as again you do not instantiate child coordinators at all and you do not need to deallocate them.

If however for some reason you decide that you still want to go with child coordinators way then for reference here are few links to articles about possible solutions to back button problem when child coordinators are used:

Arias answered 2/7, 2021 at 17:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.