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.
Related
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.
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
I have a tabBar with a tab that contains a NavVC that has a root as a ParentVC. The ParentVC has a segmentedControl that manages two childVCs, RedVC and BlueVC. Both the RedVC and BlueVC contain a button to push on a YellowVC.
The issue is when the YellowVC gets pushed on, and in it's viewDidLoad I check to see the view controllers on the stack, the two controllers that appear are the ParentVC and the YellowVC, there is no mention of either The RedVC (if it pushes it on) or the BlueVC (if it pushes it on).
for controller in navigationController!.viewControllers {
print(controller.description) // will only print ParentVC and YellowVC
}
I understand that since the tabBar has a navVC as it's tab and it's root is the ParentVC then that's the root of the push but I need to know which one of either the RedVC or the BlueVC triggered it when the YellowVC gets pushed on.
I can use some class properties in the YellowVC but I want to see if there's another way via the navigationController:
var pushedFromRedVC = false // I'd prefer not to use these if I don't have to
var pushedFromBlueVC = false
How can I get a reference to the RedVC or BlueVC when either of them push on the YellowVC?
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let parentVC = ParentVC()
let navVC = UINavigationController(rootViewController: parentVC)
navVC.tabBarItem = UITabBarItem(title: "Parent", image: nil), tag: 0)
viewControllers = [navVC]
}
}
class ParentVC: UIViewController {
var segmentedControl: UISegmentedControl! // switching segments will show either the redVC or the blueVC
let redVC = RedController()
let blueVC = BlueController()
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(redVC)
view.addSubview(redVC.view)
redVC.didMove(toParentViewController: self)
addChildViewController(blueVC)
view.addSubview(blueVC.view)
blueVC.didMove(toParentViewController: self)
}
}
class RedVC: UIViewController {
#IBAction func pushYellowVC(sender: UIButton) {
let yellowVC = YellowVC()
yellowVC.pushedFromRedVC = true // I'd rather not rely on this
navigationController?.pushViewController(yellowVC, animated: true)
}
}
class BlueVC: UIViewController {
#IBAction func pushYellowVC(sender: UIButton) {
let yellowVC = YellowVC()
yellowVC.pushedFromBlueVC = true // I'd rather not rely on this
navigationController?.pushViewController(yellowVC, animated: true)
}
}
class YellowVC: UIViewController {
var pushedFromRedVC = false // only sample, I'd rather not use these if I don't have to
var pushedFromBlueVC = false
override func viewDidLoad() {
super.viewDidLoad()
for controller in navigationController!.viewControllers {
// even though the push is triggered from either the RedVC or BlueVC it will only print ParentVC and YellowVC
print(controller.description)
}
}
}
Since RedVC and BlueVC are contained in ParentVC you won't find any reference to them in the UINavigationController stack.
An alternative to using the two booleans, is to use a sourceVC property:
class YellowVC: UIViewController {
var sourceVC: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
if self.sourceVC is BlueVC {
print("We came from Blue")
} else if self.sourceVC is RedVC {
print("We came from Red")
} else {
print("We came from somewhere else"
}
}
}
class RedVC: UIViewController {
#IBAction func pushYellowVC(sender: UIButton) {
let yellowVC = YellowVC()
yellowVC.sourceVC = self
navigationController?.pushViewController(yellowVC, animated: true)
}
}
class BlueVC: UIViewController {
#IBAction func pushYellowVC(sender: UIButton) {
let yellowVC = YellowVC()
yellowVC.sourceVC = self
navigationController?.pushViewController(yellowVC, animated: true)
}
}
You may need to think about whether YellowVC needs to invoke some method on Red/BlueVC or just needs to know some state that is implied by the Red/Blue source.
If it is the latter then RedVC and BlueVC should probably use a delegation pattern to let ParentVC know that it should push YellowVC and set the appropriate state properties based on the source of the delegation call.
You could also consider putting the state into your model that is passed down to the VCs rather than discrete properties on the VCs themselves.
I need to send some data back from secondView to First View by popView.
How can i send back the data by popViewControllerAnimated?
Thanks!
You can pass data back using delegate
Create protocol in ChildViewController
Create delegate variable in ChildViewController
Extend ChildViewController protocol in MainViewController
Give reference to ChildViewController of MainViewController when navigate
Define delegate Method in MainViewController
Then you can call delegate method from ChildViewController
Example
In ChildViewController: Write code below...
protocol ChildViewControllerDelegate
{
func childViewControllerResponse(parameter)
}
class ChildViewController:UIViewController
{
var delegate: ChildViewControllerDelegate?
....
}
In MainViewController
// extend `delegate`
class MainViewController:UIViewController,ChildViewControllerDelegate
{
// Define Delegate Method
func childViewControllerResponse(parameter)
{
.... // self.parameter = parameter
}
}
There are two options:
A) with Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
let goNext = segue.destinationViewController as ChildViewController
goNext.delegate = self
}
B) without Segue
let goNext = storyboard?.instantiateViewControllerWithIdentifier("childView") as ChildViewController
goNext.delegate = self
self.navigationController?.pushViewController(goNext, animated: true)
Method Call
self.delegate?.childViewControllerResponse(parameter)
If you want to send data by popping, you'd do something like:
func goToFirstViewController() {
let a = self.navigationController.viewControllers[0] as A
a.data = "data"
self.navigationController.popToRootViewControllerAnimated(true)
}
Extending Dheeraj's answer in case your ViewController is not first VC in the stack, here is the solution:
func popViewController() {
guard let myVC = self.navigationController?.viewControllers.first({ $0 is MyViewController }) else { return }
myVC.data = "data"
self.navigationController?.popViewController(animated: true)
}
However, this solution will break if you have 2 or more than 2 MyViewController in the stack. So, use wisely.
Answer given here is a little complex, quite simple to just use UINavigationControllerDelegate
class FirstNavigationController: UIViewController {
var value: String?
}
class SecondNavigationController: UIViewController, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
guard let vc = navigationController.topViewController as? FirstNavigationController else { return }
vc.value = "Hello again"
}
}
self.navigationController?.popViewController(animated: true)
let vc = self.navigationController?.viewControllers.last as! MainViewController
vc.textfield.text = "test"
this popviewcontroller solutions