Can not remove child coordinator because transitionCoordinator is nil in navigationController delegate method

1.3k views Asked by At

Brief :

I have implemented Soroush's Coordinators architecture. Everything works fine except the removing part which is needed to remove previous(child) coordinators.

Scenario :

I have two ViewController named HomeViewController and MyGroupsViewController. Each has its own coordinator named HomeCoordinator and MyGroupsCoordinator respectively.

User taps a button on HomeViewController which triggers gotoMyGroupsTapped function and gets the user to MyGroupsViewController, Then the user taps on another button on MyGroupsViewController which get the user back to HomeViewController by triggering gotoHomePage().

Pretty simple! : HomeVC -> MyGroupsVC -> HomeVC

But the Problem is :

navigationController.transitionCoordinator? is nil in func navigationController(..., didShow viewController: UIViewController...) in both coordinators and I can not remove child coordinators in each transition.

Is it correct to set navigationController.delegate = self in start() func of both coordinators?

Should I use navigationController?.popViewController(animated: false ) in my backToHomePage() func? because Paul Hudson has only used pushViewController.

My Codes [Simplified Versions]:

HomeCoordinator.swift

import Foundation
import UIKit

class HomeCoordinator: NSObject,Coordinator,UINavigationControllerDelegate {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController
    weak var parentCoordinator : Coordinator?
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

     func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
         // Transition here is nil
         print(" Transition : ",navigationController.transitionCoordinator)
         guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
             print("Unknown fromViewController!")
             return
         }
         // Removing a child coordinator
     }

    func gotoMyGroups (){
         let groupsCoordinator = GroupsCoordinator(navigationController: navigationController)
         childCoordinators.append(groupsCoordinator)
         groupsCoordinator.parentCoordinator = self
         groupsCoordinator.start()
      }

     func start() {
        let vc = HomeViewController.instantiate()
        vc.coordinator = self
        navigationController.delegate = self
        navigationController.pushViewController(vc, animated: false)
        navigationController.setNavigationBarHidden(true, animated: false)
     }
}

MyGroupsCoordinator.swift

import Foundation
import UIKit

class MyGroupsCoordinator: NSObject,Coordinator,UINavigationControllerDelegate {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController
    weak var parentCoordinator : Coordinator?
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

     func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
         // Transition here is nil
         print(" Transition : ",navigationController.transitionCoordinator)
         guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
             print("Unknown fromViewController!")
             return
         }
         // Removing a child coordinator
     }

     func start() {
        let vc = MyGroupViewController.instantiate()
        vc.coordinator = self
        navigationController.delegate = self
        navigationController.pushViewController(vc, animated: false)
        navigationController.setNavigationBarHidden(true, animated: false)
     }
}

MyGroupViewController.magik

class MyGroupViewController :  UIViewControllerWithCoordinator,UITextFieldDelegate,Storyboarded{

     @IBAction func gotoHomePage(_ sender: Any) {
         if let coord = coordinator as? GroupsCoordinator {
             coord.parentCoordinator?.start()
         }
     }
 }

HomeViewController.swift

 class HomeViewController: UIViewControllerWithCoordinator,Storyboarded {
     @IBAction func gotoMyGroupsTapped(_ sender: Any) {
         guard let acoordinator = coordinator as? HomeCoordinator else {
             return
         }
         acoordinator.gotoMyGroups()
     }
1

There are 1 answers

0
Ben On

It looks to me there is a confusion around Coordinator pattern usage here.

From your expected flow HomeVC -> MyGroupsVC -> HomeVC, if you mean in the sense level1 -> level2 -> level3, then GroupsCoordinator should create a new HomeCoordinator instance with its own new HomeVC.

So instead of your previous code

class MyGroupViewController ... {
    @IBAction func gotoHomePage(_ sender: Any) {
        if let coord = coordinator as? GroupsCoordinator {
            coord.parentCoordinator?.start()
        }
    }
}

I would change it to

class MyGroupViewController ... {
    @IBAction func gotoHomePage(_ sender: Any) {
        if let coord = coordinator as? GroupsCoordinator {
            coord.goToHome()
        }
    }
}

class MyGroupsCoordinator ... {

    func goToHome() {
        let homeCoordinator = HomeCoordinator(navigationController: navigationController)
        childCoordinators.append(homeCoordinator)
        groupsCoordinator.parentCoordinator = self
        groupsCoordinator.start()
    }
}

This will allow you to create a brand new page as you describe there HomeVC -> MyGroupsVC -> HomeVC.


However, if you meant in this approach level1 -> level2 -> (back) level1, then you'll need to terminate MyGroupsCoordinator and remove from the parent while navigating back.

As you noticed, to do so, you'll need to use UINavigationControllerDelegate to be able to be notified when the user navigate back (either pop in code, or with classic back button).

One solution I found is to use a Router to handle all this navigation when a UIViewController is removed from it to also notify via closures the right coordinator to be removed. You can read more about it here.

Hope it helps