I use coordinator pattern with child coordinators. I have a problem with removing child coordinator of my child coordinator.
This is a sequence of my coordinators:
HomeCoordinator -> RoutineCoordinator (child of HomeCoordinator) -> ExerciseCoordinator (child of RoutineCordinator) -> CustomExerciseCoordinator (child of ExerciseCoordinator)
To get know when user pops view controllers I use method didShow from navigation controller delegate.
When I push view controllers, everything is ok, but I when I move back, method didShow is called only once. When I use back button twice, the second time didShow is not called.
Example:
I move back from CustomExerciseCoordinator to ExerciseCoordinator and didShow works properly. Then I immediately move back to previous coordinator (RoutineCoordinator) and didShow is not called.
I am not sure if it is needed to show all coordinators, because the code for each coordinator looks similar, but below it is shown.
class HomeCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.tintColor = .white
super.init()
navigationController.delegate = self
}
func start() {
let vc = HomeFactory.makeHomeScene(delegate: self)
navigationController.pushViewController(vc, animated: false)
}
}
extension HomeCoordinator: HomeCoordinatorDelegate {
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
class NewRoutineCoordinator: NSObject, Coordinator {
var exerciseNumber: Int?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
weak var passWorkoutToHomeDelegate: PassWorkoutToHome?
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewRoutineFactory.makeNewRoutineScene(delegate: self)
navigationController.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(vc, animated: true)
}
}
extension NewRoutineCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewRoutineViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewExerciseFactory.newExerciseScene(delegate: self)
navigationController.pushViewController(vc, animated: true)
}
extension ExerciseCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewExerciseViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
I guess something is nil the second time you navigate back. This often happens to me when I instantiate a view controller inside a function which it gets de-initialized so my delegation doesn't work anymore. What I usually do is declare a variable outside the function and assign the instantiated view controller to it.
For Instance try:
var newRoutineCoordinator: NewRoutineCoordinator?
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
newRoutineCoordinator = child
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
Not sure the problem is there but this is just to give you an example.
You have UINavigationControllerDelegate inside an extension of NewRoutineCoordinator but It may be already nil by the time you navigate back.
Related
I use coordinator pattern with child coordinators to provide loosely coupled code and accordant with single responsibility principle. To handle with navigating back I've decided to use navigation controller delegate (accordingly with this article https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios).
But my problem is that I have Home VC with Home Coordinator, next I go to Workout VC with Workout Coordinator and next I go to Exercise VC with Exercise Coordinator.
So I want to have a Workout Coordinator and Exercise Coordinator as children for Home Coordinator.
And everything seems to be ok, but I have a memory leak between the Workout Coordinator and Exercise Coordinator because I can't delete the exercise coordinator after I use the back button to navigate back. My navigation controller delegate doesn't recognize when I back.
class HomeCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.white]
}
func start() {
navigationController.delegate = self
let vc = HomeFactory.makeHomeScene(delegate: self)
navigationController.pushViewController(vc, animated: false)
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return }
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let addWorkoutViewController = fromViewController as? NewWorkoutViewController {
let workoutCoordinator = addWorkoutViewController.presenter?.workoutCoordinatorDelegate
childDidFinish(workoutCoordinator)
}
}
}
extension HomeCoordinator: HomeCoordinatorDelegate {
func goToWorkoutCreating() {
let child = AddWorkoutCoordinator(navigationController: navigationController)
child.passWorkoutToHomeDelegate = self
childCoordinators.append(child)
child.parentCoordinator = self
child.start()
}
}
class AddWorkoutCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
var parentCoordinator: Coordinator?
var exerciseNumber: Int?
weak var passWorkoutToHomeDelegate: PassWorkoutToHome?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.navigationBar.tintColor = .white
}
func start() {
let vc = AddWorkoutFactory.makeAddWorkoutScene(delegate: self)
navigationController.pushViewController(vc, animated: true)
}
func childDidFinish(_ child: Coordinator?) {
for (index, coordinator) in childCoordinators.enumerated() {
if coordinator === child {
childCoordinators.remove(at: index)
break
}
}
}
// THIS FUNCTION IS NOT RESPOND AT ALL, BUT IT RESPONDS IN HOME COORDINATOR
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return }
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let newExerciseViewController = fromViewController as? NewExerciseViewController {
// childDidFinish(newExerciseViewController.presenter?.exerciseCoordinatorDelegate)
// childDidFinish(newExerciseViewController.presenter!.exerciseCoordinatorDelegate)
}
}
}
extension AddWorkoutCoordinator: AddWorkoutCoordinatorDelegate {
func goToAddExercise() {
let child = AddExerciseCoordinator(navigationController: navigationController)
child.passExerciseToWorkoutDelegate = self
childCoordinators.append(child)
child.start()
}
class AddExerciseCoordinator: NSObject, Coordinator {
weak var passExerciseToWorkoutDelegate: PassExerciseToWorkoutDelegate?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
self.navigationController.navigationBar.tintColor = .white
}
func start() {
let vc = AddExerciseFactory.makeAddExerciseScene(delegate: self)
navigationController.pushViewController(vc, animated: true)
}
func startEditExercise(setData: [ExerciseFieldsModel], exerciseName: String) {
let vc = AddExerciseFactory.makeAddExerciseScene(delegate: self)
vc.presenter?.textFieldsModel = setData
vc.exerciseText = exerciseName
navigationController.pushViewController(vc, animated: true)
}
}
extension AddExerciseCoordinator: NewExerciseCoordinatorDelegate {
func saveExercise(newExercise: ExerciseCellInNewWorkout) {
navigationController.popViewController(animated: true)
passExerciseToWorkoutDelegate?.passExerciseToWorkout(exercise: newExercise)
}
}
So basically the problem is that using navigate controller delegate didShow method I need to get the type of "fromViewController", so if I have one child like Workout Coordinator, everything is ok, I cant remove it easily, but when I have one layer deeper, so I have Workout Coordinator and after that Exercise Coordinator I don't know how to use navigation controller delegate didShow method to recognize if Exercise Coordinator is popped. So I have tried to use navigation controller didShow also in Workout Coordinator, but as I wrote in a comment in code, it doesn't respond at all.
Could you help me to get some advice on how to detect if the user taps the back button on exercise VC? Or maybe my conception is bad and only the workout coordinator should be a child and the exercise coordinator should not be.
When your ExerciseViewController goes out of scope (gets deleted) it should be notifying the object that pushed it on the navigation controller. It looks like that is your ExerciseCoordinator.
Setup your Coordinator containment system the same way UIKit's view containment system is setup. Where a Coordinator has an addChild(coordinator:) method and a removeFromParent() method. When the view controller notifies its coordinator that it went out of scope, the coordinator should call its removeFromParent() method.
The system above is far better than relying on didShow and works for any sort of method of presentation.
I use coordinator pattern in my app, but I have problem with instantiate view controllers. The problem is that I use different module for each tab bar controller.
So far I've used this approach
protocol Storyboarded {
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
And during creation tab bar coordinator:
class MainTabBarController: UITabBarController, Storyboarded {
let main = MainCoordinator(navigationController: UINavigationController())
let calendar = CalendarCoordinator(navigationController: UINavigationController())
let chart = ChartCoordinator(navigationController: UINavigationController())
let profile = ProfileCoordinator(navigationController: UINavigationController())
override func viewDidLoad() {
super.viewDidLoad()
main.start()
calendar.start()
chart.start()
profile.start()
viewControllers = [main.navigationController, calendar.navigationController, chart.navigationController, profile.navigationController]
}
My all view controllers conform to Storyboarded Protocol:
class HomeTableViewController: UIViewController, Storyboarded {}
And coordinator for each tab bar controller looks like this
class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeTableViewController.instantiate()
vc.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "home"), tag: 0)
navigationController.pushViewController(vc, animated: false)
}
}
The problem is that other tab bar controllers belong to others storyboard, not only "Main". Using instantiate() from protocol causes error. I wonder how to create protocol extension where I can initialise ViewControllers with different storyboard names, not only "Main".
Try add this:
protocol Storyboarded {
static var storyboardName: String { get }
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static var storyboardName: String {
"Main" // Default implementation
}
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: Self.storyboardName, bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
Now you can do:
extension HomeTableViewController: Storyboarded {
class var storyboardName: String {
"Home" // Other storyboard name here, overrides default implementation
}
}
I have been trying to refactor my source code so that it would conform to the Coordinator Pattern. I have used UITabBarController as the parent viewController of my app which contains 4 viewControllers.
I have been following the tutorials on how to implement the Coordinator pattern for iOS apps, and I have created and set up the protocols and classes of the Coordinator. I have a button inside my viewController (child viewController of the TabbarViewController), however, on button click, coordinator is not pushing / navigating to the desired VC, and I see the coordinator is returning nil on the debug console while debugging through the breakpoint, and I could not figure it out how to resolve this issue.
MainCoordinator.swift:
class MainCoordinator: SubCoordinator {
var subCoordinators = [SubCoordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
print("Initialized.. .")
UIApplication.app().window?.rootViewController = self.navigationController
let vc = SplashViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
// testing using a simple Viewcontroller class, its background color is set to red, so if the
// navigation works, a blank red VC should appear. but not working so far
func testView() {
let vc = ViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
SubCoordinator.swift:
protocol SubCoordinator {
var subCoordinators: [SubCoordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
StoryBoarded.swift:
protocol StoryBoarded {
static func instantiate() -> Self
}
// I am using storyBoard, and `instantiate()` should instantiate and return the specified VC
// from the Storyboard with the specified VC id (?)
extension StoryBoarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: id) as! Self
}
}
FirstViewController.Swift:
class FirstViewController: UIViewController, StoryBoarded {
#IBOutlet weak var button: UIButton!
var coordinator: MainCoordinator?
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// If uncommented the below line, coordinator is not returning `nil`, but not navigating
anyways!
//coordinator = MainCoordinator(navigationController: UINavigationController())
}
#IBAction func onButtonTap(_ sender: Any) {
// So, basically I would expect the coordinator to navigate to the testView, but not
navigating
coordinator?.testView()
}
}
ViewController.swift:
// testView
class ViewController: UIViewController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.backgroundColor = .red
}
}
and
// TabbarController, set as the root VC after the splashVC is completed
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = UIStoryboard.firstViewController()
let secondVC = UIStoryboard.secondViewController()
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}
start() is being called, and splashVC appears and updates rootViewController with MainViewontroller on completion, But the navigation is not working at all on button click event.
Any feedback or help would highly be appreciated!
Since you're using the StoryBoarded protocol, you should follow the pattern and call instantiate() for initialization. Then, just set the coordinator.
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = FirstViewController.instantiate()
let secondVC = SecondViewController.instantiate()
firstVC.coordinator = self.coordinator
secondVC.coordinator = self.coordinator
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}
I'm trying to make custom transitions when pushing/popping viewControllers from a custom UINavigationController class. I'm implementing the UINavigationControllerDelegate method
navigationController(_:animationControllerFor:from:to:), but it does not get called.
I'm creating a UINavigationController in storyboard and putting it's class as CustomNavigationController. I'm also assigning it a root ViewController in the storyboard (let's call the root VC CustomViewControllerRoot).
Here is the code I'm using (simplified and not tested):
protocol NavigationDelegate {
func pushCustomViewController()
func popViewController()
}
class CustomNavigationController: UINavigationController, NavigationDelegate {
init() {
super.init(nibName: nil, bundle: nil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
self.navigationBar.isHidden = true
guard viewControllers.first is CustomViewControllerRoot else {fatalError("Error")}
rootVC = viewControllers.first as? CustomViewControllerRoot
rootVC?.navigationDelegate = self
//Setup the rest of the viewControllers that are to be used
customVC = CustomUIViewController()
customVC?.navigationDelegate = self
}
var rootVC: CustomViewControllerRoot?
var customVC: CustomViewController?
func pushCustomViewController() {
if customVC != nil {
self.pushViewController(customVC!, animated: true)
}
}
func popViewController() {
self.popViewController(animated: true)
}
}
extension CustomNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// This is never called, even though the delegate is set in the initializer to CustomNavigationController
print("TEST")
return nil
}
}
I then let each custom UIViewController subclass in my navigation hierarchy delegate push or pops to this CustomNavigationController above. For example this is the root vc assigned to the navigation controller. Since it lies as root it never needs to push itself or be popped, as it is presented when the CustomNavigationController is presented. It delegates to CustomNavigationController when it finds that another VC should be presented on top of it:
class CustomViewControllerRoot {
var navigationDelegate: NavigationDelegate?
override func viewDidLoad(){
super.viewDidLoad()
}
#objc func someButtonPressedToPresentCustomVC(){
navigationDelegate?.pushCustomViewController()
}
}
The dismissal is handled inside each CustomViewController which also delegates the pop down to the CustomNavigationController (I don't want to use the navbar for dismissal so there is no "back button" from the start):
class CustomViewController: UIViewController {
var navigationDelegate: NavigationDelegate?
override func viewDidLoad(){
super.viewDidLoad()
}
#objc func dismissViewController(){
navigationDelegate?.popViewController()
}
}
To my understanding the UINavigationControllerDelegate method inside the extension of CustomNavigationController should be called whenever a push or pop is performed since I'm setting the delegate variable to self in the initializer?
Your navigation controller should have a root view controller.
And then you should push custom view controller from your root view controller. And delegate method calls
Navigation Controller Code:import UIKit
class CustomNV: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension CustomNV: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("TEST")
return nil
}
}
RootViewController code:
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let viewController = UIViewController(nibName: nil, bundle: nil)
viewController.view.backgroundColor = .green
navigationController?.pushViewController(viewController, animated: true)
}
}
Set root view controller as root for navigation controller in storyboard
Here is a UIViewController where a UINavigationController has been created:
class Main_ProfileViewController: UIViewController {
var ProfileNavigationController = UINavigationController()
buildProfileNavigationController()
func buildProfileNavigationController() {
let RootViewController = BlankViewController200()
ProfileNavigationController = UINavigationController(rootViewController: RootViewController)
self.view.addSubview(ProfileNavigationController.view)
}
}
And here is the main container class with the navigation controller delegate to get the current top of the stack:
class MainContainerViewController: UIViewController {
var currentTop: UIViewController?
}
extension MainContainerViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
currentTop = viewController
print(currentTop)
}
}
However, nothing is ever printed to the console. How is this achieved?
For getting top viewController you can use this code:
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}