[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:
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.
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.
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.
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.
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.
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:
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.
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 :
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.
Go to the properties inspector. Change it to Pizza.
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.
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
.
The controller changes to an OrderTabController
.
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:
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:
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 } }
Leave a Reply to kevin Cancel reply