Swift Swift Tutorials: Passing Data in Tab Bar Controllers

[Updated for Swift 2.0/iOS9 9/21/15 SJL]

While you can delegate between view controllers in tab bar controllers, it’s debatable if you want to. Tab bar controllers can break down MVC in cases. On one hand Apps with tab bar controllers have independent view controllers with completely separate functions, much like Apple’s clock app. Though the theme is time, the stopwatch has nothing to do with the countdown timer. They do not share data and thus have completely independent models. On the other hand there may be one common model among all or some of the tabs which uses it differently. The music app uses the same model arranged differently in the song, album, and artist tabs .
View Controllers on a tab bar, unlike Navigation controllers or Modal views, exist parallel to each other due to the array viewControllers . When they disappear from the screen, they are not destroyed. When they appear, they are not loaded. Between sharing a model and not getting destroyed, there are better ways to work with a model in a tab bar than the delegation we use in other view controllers.

Set up the Demo

Make a new project by either going to File>New>Project… or Command-Shift-N on the keyboard. Make a single view project called SwiftTabDataOrderDemo. Make the language Swift and the device Universal. Save the project.

Layout the Views

Go to the storyboard. Select the single view controller by clicking on the title. In the drop-down menu, select Editor>Embed in>Tab Bar Controller.
I will use auto layout to set this up. If you are not interested in using auto layout, set the class sizes at the bottom of the storyboard for Compact Width, any height:

Screenshot 2015-01-30 05.49.45

In this size class you can drag a drop and be pretty sure it will show correctly on a iPhone simulator. If you plan to follow the auto layout leave the class size Width: Any Height:Any.
Add two labels, one with Pie for its text and one left unlabeled. Add two buttons titled Apple and Cherry. Use white for the text color and a blue of 50% alpha for the button background color to make a blended background color. Arrange the buttons and labels so they look like this.

Screenshot 2015-01-30 06.09.35

If you are not using auto layout skip to Duplicate the View. If you are, select the pie label. In the align menu, Select Horizontal Center in Container, and Vertical Center in Container. Make sure both are zero. In Update Frames, select Items of new constraints.

Screenshot 2015-01-30 05.59.58

Then add the constraints. Select the untitled label. In the pin menu, set the Label 10 points up, 0 points left, and 0 points right. Make the height 42 points. Select Items of New Constraints for Update Frames.

Screenshot 2015-01-30 06.14.48

Now Select the Apple button. Control-drag up and to the left into the label until the label highlights, then release. In the menu that pops up, shift select, Vertical Spacing, Left, Equal Heights And press Return. Control drag from the Apple Button to the Cherry button. Shift-select Horizontal Spacing, Baseline, Equal Widths, Equal Heights and press Return. Edit the constants so it has these values.

Screenshot 2015-01-29 06.49.29

Any constant that does not show a name of a view instead of a number change to 0 for the constant.
Select the Cherry Button. Control drag directly right from Cherry to the View. In the popup, select Trailing space. Edit the constraints so they have the following values.

Screenshot 2015-01-29 06.50.24

Most likely you will need to change the trailing space only, as we made most of these changes when we constrained it to Apple. In the resolver, Update all frames in the View. We now have this layout:

Screenshot 2015-01-29 06.54.30

Duplicate the View

Select the view controller icon for our Pie View Controller in the storyboard. Press Command-C to copy the view. Deselect the view by selecting the white of the storyboard. Press Command-V to paste a copy of the view controller. The icons at the top of the controller should highlight. Drag from the top icon bar down and to the right. You’ll find you made a copy of the view controller.

Screenshot 2015-01-30 06.33.53

If the controllers overlap, Drag the controllers apart so they do not overlap. On the new controller, Change Pie to Pizza, and Apple and Cherry to Cheese and Pepperoni respectively.Change the background color to red, and the two labels to white text color :

Screenshot 2015-01-29 07.18.25

Press Command-N to make a new file. Select a Cocoa Touch Class. Make a new UIViewController Subclass called PizzaViewController. Leave the language Swift. Save the file and go back to the storyboard when the code view shows. Click on the view controller icon for the Pizza view, and in the identity inspector, change the view controller to PizzaViewController.

Configure the Tab Bar

Now from the TabBarController on the storyboard, control-drag to the Pizza view. Select a Relationship Segue View Controllers. You will now have a tab bar at the bottom of the pizza view. In the document outline select Item.

Screenshot 2015-01-30 06.59.01

Go to the properties inspector. Change it to Pizza.

Screenshot 2015-01-30 06.56.34

Select the tab bar in the Pie view. Do the same and title the tab bar item Pie.

Wire Up the Views

If not already open, open the Assistant editor and set to Automatic in the upper toolbar. Select the Pizza View in the storyboard half. You should see the PizzaViewController.swift file on the right. Control-drag from the label to the code and make an outlet orderLabel. Control-drag from the Cheese Button and make an action as a UIButton for Sender of orderButton. Drag from the circle left of the orderButton to the Pepperoni button and release when the button highlights.
Select the Pie view controller in the storyboard. You’ll see ViewController.swift in the Assistant editor. Do the same to connect up the labels and buttons there.

Make an Order Model

Let’s make a simple model for another pizza and pie ordering app. Press Command-N to Create a new file. Select a Cocoa Touch Class, and subclass NSObject with a class called OrderModel. Change the code for OrderModel to look like this:

class OrderModel: NSObject {
    var pizza:String = "No"
    var pie:String = "No"

    func currentOrder() -> String{ //return a string with the current order
        return pizza + " pizza and " + pie + " pie"
    }
}

This model has two properties and one method. We’ll store an order for one pizza and one pie, and have a method to return what our order is.
Go into the PizzaViewController class. Change the code for the class to the following:

class PizzaViewController: UIViewController {
    var myOrder = OrderModel()

    @IBOutlet weak var orderLabel: UILabel!

    @IBAction func orderButton(sender: UIButton) {
        myOrder.pizza = (sender.titleLabel?.text)!
        orderLabel.text = myOrder.currentOrder()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        orderLabel.text = myOrder.currentOrder()
    }

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

The first line of the class sets up a property with our model class.
All the action happens in our action orderButton. We press a button, and the title of the button becomes our order for pizza. . We retrieve the complete order from the model and display it in a label.
Instead of the viewDidLoad method, here we use viewWillAppear. We don’t load the view every time the tab appears, the view remains alive as we switch views. In a Tab Bar Controller, viewDidLoad only happens once for each child view. We can guarantee that viewWillAppear will happen every time the view appears, and thus it is the place to put our code to update the label.
This was the second controller. Let’s look at the code for the first controller to load and appear. Change the ViewController.swift file as follows:

class ViewController: UIViewController{
  var myOrder = OrderModel()
  // MARK: - Target Actions

  @IBOutlet weak var orderLabel: UILabel!

  @IBAction func orderButton(sender: UIButton) {
    myOrder.pie = (sender.titleLabel?.text)!
    orderLabel.text = myOrder.currentOrder()
  }
  //MARK: - Life Cycle
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    orderLabel.text = myOrder.currentOrder()
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    let barViewControllers = self.tabBarController?.viewControllers
    let svc = barViewControllers![1] as! PizzaViewController 
    svc.myOrder = self.myOrder  //shared model

  }
}

The code is almost the same here with the exception of the viewDidLoad method. We use some very important properties for us. We have a pointer to the Tab bar controller in the UIViewController property tabBarController, which is an optional. In the Tab bar controller is the array of controllers. This line

let barViewControllers = self.tabBarController?.viewControllers

gets us that array. Since we only have two view controllers, We know the second view controller is the PizzaViewController class. We downcast the result of that from AnyObject to PizzaViewController. We set the pointer from our model in the PizzaViewController to point to the model in ViewController. Now they share data.
You should be all set up to run now. Build and run, and you should get a working app.
Screenshot 2015-01-29 07.26.36 Screenshot 2015-01-29 07.27.10

Problems with Sharing Data

For two tabs this code works fine. For three through five it starts to get clunky. Suppose we had another view controller CoffeeViewController. We would declare it in viewDidLoad like this( though don’t):

  override func viewDidLoad() {
    super.viewDidLoad()
    let barViewControllers = self.tabBarController?.viewControllers
    let svc = barViewControllers![1] as! PizzaViewController //20
    svc.myOrder = self.myOrder  //shared model
    let svc2 = barViewControllers[2] as! CoffeeVC
    svc2.myOrder = self.myOrder

  }

We can keep adding view controllers manually here like this. It’s repetitive work and not very stylish, but it works for simple cases.
However, if you go above five tabs,this fails. The user can change the order and visibility of the tabs with the edit selection in the More tab. Once the order changes, we cannot tell what view controller we need for each element in the array. There are properties which let us stop the user from reordering the view, but again, that’s a bit of repetitive code.
There must be a better way.

Using the AppDelegate and Global Models

There is another way to handle sharing data among a tab bar controller you will hear a lot about: Add the model to the app delegate. The model becomes global. Global variables are accessible from everywhere in your app, including things you don’t want to access the data. This way, the model is global and all the view controllers can access it. The way to tell a bad programmer is global variables. They are bad for style, bad for security of your data and a nightmare for self-documentation. You will find a lot of internet solutions which use global models in one form or the other. Except in some very special circumstances, avoid global variables. Global variables make code very confusing and very complex very fast.

Subclassing UITabBarController

The reason people like global variable for a tab bar controller is that the data is accessible from all the view controllers. Problem is, it is accessible from everywhere else as well. Thinking in terms of encapsulation, can we restrict the data to just the view controllers? The answer is yes. It also is very simple: subclass UITabBarController.
Make a new file by pressing Command-N. Make a file named OrderTabBarController subclassing UITabBarController, with Swift as a the language. You may have to scroll a bit or type the class in, since it is buried in the drop down for subclass. You’ll get a new class. Remove the entire class and replace the class with this:

class OrderTabController: UITabBarController {

    var myOrder = OrderModel()
}

We added to UITabBarController our model. Go to the storyboard, and click the Tab bar controller. in the identity inspector, change UITabBarController to OrderTabBarController.

Screenshot 2015-01-31 16.30.22

The controller changes to an OrderTabController.

Screenshot 2015-01-31 16.30.51

We replaced the default tab bar controller with a tab bar controller that has one extra property: our model. Our view controllers can access the OrderTabController by the tabBarController property. From there, they can get the model.
Go to PizzaViewController.swift. Make the viewDidLoad method look like this:

override func viewDidLoad() {
    super.viewDidLoad()
    let tbvc = self.tabBarController  as! OrderTabController
    myOrder = tbvc.myOrder
}

Since the system thinks this is a generic tab bar controller, we need to downcast to use the model. Once downcast, we set the reference for the model to the one in OrderTabController. Once we do this, our model for this controller is the shared model.
Go to ViewController.swift. Comment out the earlier code and add the same two lines we entered in PizzaViewController’

override func viewDidLoad() {
        super.viewDidLoad()
        //let barViewControllers = self.tabBarController?.viewControllers
        //let svc = barViewControllers![1] as! PizzaViewController //20
        //svc.myOrder = self.myOrder  //shared model
        let tbvc = tabBarController as! OrderTabController
        myOrder = tbc.myOrder
    }

Build and run. You get the same results as before. You can even add more view controllers and it works just fine. Let make a quick summary sheet for the order. Go to the storyboard. Drag out another view controller. Control-drag from Order Tab Controller to the new controller. Drag out five labels, and arrange them like this:

Screenshot 2015-01-31 18.11.19

If you want to add auto layout to this I’ll leave that up to taste, but this time I’ll do without. Click the bar item and in the properties inspector, make the title Summary. Press Command-N and make a new Cocoa Touch Class file named SummaryViewController which subclasses UIViewController, with Swift as the Language. Change the class to the following:

class SummaryViewController: UIViewController {
    var myOrder = OrderModel()
    @IBOutlet weak var pieOrder: UILabel!
    @IBOutlet weak var pizzaOrder: UILabel!

    override func viewWillAppear(animated: Bool) {
        pieOrder.text = myOrder.pie
        pizzaOrder.text = myOrder.pizza
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        myOrder = (tabBarController as! OrderTabController).myOrder
        // Do any additional setup after loading the view.
    }
}

This code should make sense by now. We won’t use the currentOrder method this time, but send each of the properties directly to a label. We also compressed the line in viewDidLoad into one line instead of two.
Go back to the storyboard. Select the view controller icon for the Summary tab. In the identity inspector, change the class to SummaryViewController. Hide the identity inspector and open up the Assistant Editor. Drag from the outlet circles to the proper label.
You just added another view controller to your tab. Build and run. Pick a Cherry Pie and a Cheese pizza and you get on the summary:

Screenshot 2015-01-31 18.33.03

adding view controllers with a shared model is that easy.

Adding a Shared Model Programmatically

So far,we’ve done this on the storyboard. If you wanted to do this programmatically from the last chapter, read Using Tab Bar Controllers in Swift which will give an example. In short though, you change this in the AppDelegate

let tabBarController = UITabBarController()

to this:
[/code]let tabBarController = OrderTabBarController()[/code]
and everything else would be the same.

The Whole Code

OrderTabController.swift


class OrderTabController: UITabBarController {
    let myOrder = OrderModel()
}

OrderModel.swift

class OrderModel: NSObject {
    var pizza:String = "No"
    var pie:String = "No"

    func currentOrder() -> String{ //return a string with the current order
        return pizza + " pizza and " + pie + " pie"
    }
}

ViewController.swift

class ViewController: UIViewController {
    var myOrder = OrderModel()

    @IBOutlet weak var orderLabel: UILabel!

    @IBAction func orderButton(sender: UIButton) {
        myOrder.pie = (sender.titleLabel?.text)!
        orderLabel.text = myOrder.currentOrder()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        orderLabel.text = myOrder.currentOrder()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        //let barViewControllers = self.tabBarController?.viewControllers
        //let svc = barViewControllers![1] as! PizzaViewController //20
        //svc.myOrder = self.myOrder  //shared model
        let tbc = tabBarController? as! OrderTabController
        myOrder = tbc.myOrder
    }
}

SummaryViewController.swift

class SummaryViewController: UIViewController {
    var myOrder = OrderModel()
    @IBOutlet weak var pieOrder: UILabel!
    @IBOutlet weak var pizzaOrder: UILabel!

    override func viewWillAppear(animated: Bool) {
        pieOrder.text = myOrder.pie
        pizzaOrder.text = myOrder.pizza
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        myOrder = (tabBarController as! OrderTabController).myOrder
        // Do any additional setup after loading the view.
    }

    }

PizzaViewController.swift

class PizzaViewController: UIViewController {
    var myOrder = OrderModel()

    @IBOutlet weak var orderLabel: UILabel!

    @IBAction func orderButton(sender: UIButton) {
        myOrder.pizza = (sender.titleLabel?.text)!
        orderLabel.text = myOrder.currentOrder()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        orderLabel.text = myOrder.currentOrder()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        let tbc = self.tabBarController  as! OrderTabController
        myOrder = tbc.myOrder
    }
}

28 thoughts on “Swift Swift Tutorials: Passing Data in Tab Bar Controllers”

    1. It is in one piece. There is a side note at the end for very experienced programmers, which does refer to an earlier post. The tabBarController property of UIViewController is a factory method. Only way you could not get it is not subclassing UIViewController in your class or You are trying to use it in the AppDelegate. If it is the second case, I would suggest learning more about the app delegate before you do anything with it, or stay clear away from it. I personally try to avoid it. There was one change to the code I did need to make. It is now upgraded to Swift 1.2. Most places where I downcasted from tabBarController with as is now as!

  1. Excellent! I’ve been working with a tab bar controller with 3 tabs and was using global variables to store some default settings and such, but knew there must be a better way and this is it. So thank you for laying it out very clearly. :)

  2. Well-written and understandable. My only concern is that with this approach, the view controllers depend on the existence of a parent tab bar controller from which they “pull” their dependency (i.e. the order model). Would a better approach be for the tab bar controller to inject the order model into the view controllers at some logical point?

    1. I’m curious. I still can’t see how you would inject a shared model in various and probably unknown subclasses of view controllers. As far a dependency, the Tab bar controller is the parent of all the view controllers that have tabs. It is always going to be there whether a subclass or not. It’s mandatory to depend on it, so a subclass of it for any particular set of tabs is not a big deal.

  3. This code segment has solved a problem I’ve had in tab controller based view controller model communication. Thanks for the great tutorial!

    Question: The model is initialized in the tab controller. Would it be better to use an optional for “myOrder” in each of the view controllers?

    I.e., declare it in each view controller as:
    var myOrder: OrderModel?

    It’s probably true that when you de-reference each newly created myOrder object, it’s memory managed out, so there’s no issue with a memory leak or with thread-safety . However, an optional leads to a single OrderModel initialization in the tab controller only, which feels cleaner.

    Happy to be told I misunderstand.

    1. If you declare it as an optional, what’s the best way to unwrap it? Are you unwrapping it each time you use it? For example, in the button’s action method and viewDidLoad, using “guard let”?

      1. I usually go for optional chaining if let aConstant= bOptional{}. There’s ARC reasons for this — Especially in cases where an optional is doubly weak as in SplitViewControllers.

      2. To be more up-to date I would say guard let would do the same, so yes to your question. I haven’t played with guard let enough to know about how strong it assigns the constant. I assume the same as if let

      3. Just tried it in the Master-detail template. guard works the same as if in terms of ARC in the DetailViewController, which is one of the places ARC gets aggressive about cleaning.

  4. Hello, new to swift here… for optional chaining, would it be something like this?

    if let order = myOrder? {
    myOrder? = tbc.myOrder
    }

    1. Short answer yes. Yes that is what we are talking about, though the question mark is not necessary . The if let clause is the older way of doing it. There is a newer way using guard let.

      This would unwrap the optional safely using if let:

      if let order = myOrder {
          myOrder? = tbc.myOrder
      } else {
         print("myOrder is nil") 
      }
      

      Using guard it would look like this:

      guard let order = myOrder else {
          print("myOrder is nil")    
          return
      }
      
      The difference between the two is that once you use guard, you've got order unwrapped for the  entire scope it is in for any use. for the if you have it only in the if clause. This is not a big deal for a single unwrapped value, but if you have several you need to unwrap it can get messy.  Compare this from the detail-master template class DetailViewController in Xcode:
      
       if let detail = self.detailItem {
                  if let label = self.detailDescriptionLabel {
                      label.text = detail.description
                  }
              }

      to this:

      guard let detail = self.detailItem else {return}
              guard let label = self.detailDescriptionLabel else{return}
              label.text = detail.description
      

      Does the same thing, but with a lot less code and a lot more flexibility.

    1. Instead of a basic view controller you have two table view controllers as tabs. If you were to share that data, what I have as orderModel would either be an array or contain an array you’d use for your table. You may want a different strategy though since that give you two tables that look exactly the same on two tabs. Tab bars are parallel view controllers. Dynamic changing a single view controller for what you have in the two tabs might be a better solution. If you are changing around the same data by filtering or changing the data in the cells, I’d use a button on a toolbar or a series of buttons in a stack view to do that and use only one view with logic in your table view datasources to change the appearance. If you are drilling down through data, that’s navigation controller territory, and you’ll find how to do that here.

  5. This is most definitely not the way to go. Children should never know about their parents, what to say about getting or setting the parents’ internal data. There is a myriad of solutions to this problem (single-direction data flow, coordinators, router objects etc.) so this tutorial should probably make it clear that this is a counter-example.

    1. I understand where you are coming from. Many people seem to have real problems with inheritance and subclassing. Suppose I use your argument with UIButton. If that is true there should be no access to the the frame property of UIView from UIButton. Should we have no access to the viewControllers array from any subclass of UIViewController? There are other ways to do this, yes, many of which I find more clunky or outright dangerous, such as globals which not just the children but the entire application has access. Others are just a bit more complicated to teach. I just find this the easiest and simplest way of handling a shared model for tab bars. Would I use it for other controllers? Definitely not.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s