简体   繁体   中英

iOS Swift Coordinator pattern and back button of Navigation Controller

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)
    }
}

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..

我的方法是使用管理子协调器的根(父)协调器,因此当用户完成流程或点击后退按钮时,会调用根协调器中的委托方法,它可以清理子协调器并在需要时创建一个新的.

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.

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

In addition to other answers posted here I would propose that you could just put startNameOfYourScreen(...) methods for each view controller in your main AppCoordinator class or whatever you named it and do not bother with child coordinators at all especially if you do not have a large amount of screens in your app.

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 to moving from this screen to different screens). This way you will not make a mess as you would with additional delegate methods because you will only have a single method per screen in your main coordinator.

You can also split those functions into extensions of AppCoordinator class and put them into separate files to have a bit better organization in you project and thats it. This way you do not have any problem with back button as you do not instantiate child coordinators at all and you do not need to deallocate them.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM