Search code examples
iosswiftuitabbarcontrollerviewwillappear

passing data between viewControllers under a tabController in swift, event sequence


I have a UITabController Main, which has two UIViewControllers, A and B. In storyboard. Each controller has a corresponding swift class, Main, A, B

Main.swift is a subclass of UITabController and a delegate of UITabBarControllerDelegate. In its viewDidLoad, it sets its .delegate = self.

In Main's func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) I capture the selected tab, and take action to set some data value I wish to pass to the selected viewController's variable Y.

In ViewController class A and B, there is a var declaration for the variable Y.

In each ViewController A and B, in viewWillAppear, I populate the local custom view outlets, from this variable Y.

The reason it doesn't work is due to the order of event processing.

I assumed that when I click the tab, the Main's tabBarController function is first processed, and then the selected corresponding viewController's viewWillAppear will be processed. This is not the case.

Evidently, first the selected (A or B) viewController's viewWillAppear executes, and afterwards, the Main's tabBarController fires. This of course causes a problem because I display outlets populated by Y and then I populate Y...

This technique for having Main populate A or B with some data is preferred, because A and B should not know anything about each other. But Main has intimate knowledge of both A and B. I really don't want to do singleton/global data passing -- I prefer to handle it cleanly.

Any suggestions for how to get the order of this event sequence corrected or suggest another (proper MVC) technique?

My goal: I want to pass some data to the view hierarchy under the tab selected, and have the caller do the passing. The receiver view hierarchy should be oblivious to how it received its data.

(I have an ugly workaround whereby I cause a re-fire of the viewWillappear, but basically this causes silly flow: A.viewWillAppear --> Main.tabBarController -->force another A.viewWillAppear)

Many thanks.

Code (simplified -- this is now a solved working example)

class MainController: UITabBarController, UITabBarControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    delegate = self;
}

 override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
    print ("tabBarController didSelect")
    let viewController = viewControllers![self.selectedIndex]
    if let vc = viewController as? AViewController {
        vc.Y = "some valuable data"
    }

    else if let vc = viewController as? BViewController {
        vc.Y = "some valuable data"
    }
}
}

class AViewController: UIViewController {


@IBOutlet weak var YTextField: UITextField!
var Y: String = ""

override func viewDidLoad() {
    super.viewDidLoad()
}


override func viewWillAppear(_ animated: Bool) {
    print ("viewWillAppear A")
    super.viewWillAppear(animated)

    YTextField.text = self.Y
}
}

class BViewController: UIViewController {

@IBOutlet weak var YTextField: UITextField!
var Y: String = ""

override func viewDidLoad() {
    super.viewDidLoad()
}


override func viewWillAppear(_ animated: Bool) {
    print ("viewWillAppear B")
    super.viewWillAppear(animated)

    YTextField.text = self.Y
}
}

Solution

  • You are using:

    // this is called when the tab's ViewController is selected (*after* the tab item is selected (tapped))
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { ... }
    

    when you want to use:

    // this is called *when* the tab item is selected (tapped)
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { ... }
    

    Here is a modification to your example:

    import UIKit
    
    class MainViewController: UITabBarController, UITabBarControllerDelegate {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            delegate = self;
        }
    
        override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
    
            let n = self.selectedIndex
    
            if let tabControllers = viewControllers {
    
                if let _ = tabControllers[n] as? AViewController {
                    // *current* VC is AViewController
                    if let vcB = tabControllers[n + 1] as? BViewController {
                        vcB.Y = "some valuable data from A"
                    }
                }
    
                else if let _ = tabControllers[n] as? BViewController {
                    // *current* VC is BViewController
                    if let vcA = tabControllers[n - 1] as? AViewController {
                        vcA.Y = "some valuable data from B"
                    }
                }
    
            }
    
        }
    
    }
    
    class AViewController: UIViewController {
    
        @IBOutlet weak var YTextField: UITextField!
    
        var Y:String = ""
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            YTextField.text = Y
        }
    
    }
    
    class BViewController: UIViewController {
    
        @IBOutlet weak var YTextField: UITextField!
    
        var Y:String = ""
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            YTextField.text = Y
        }
    
    }
    

    You'll want to do some additional handling / checking to make sure you're getting to the desired tab. You may want to set a variable inside MainViewController on didSelect item and then set the variable inside the "target" view controller on didSelect viewController.