[Updated to Swift 2.0/iOS9.0 9/20/2015 SJL]
While Navigation controllers often have the limelight when it comes to Xcode’s view controllers, tab bar controllers are better for independent tasks in the same app, or for different ways of working with the same model. In this lesson we’ll take a look at tab bar controllers and after a short part about the template, how to make them in Swift programmatically. If you are interested in tab bar controllers on the storyboard, You might want to read this post. For passing data between tabs, on the storyboard can be found here.
Tab Bar Programmatically
While there are very easy storyboard ways of making tab bar controllers, We can do much of this programmatically. Start with a single view template and create a Swift project called TabProgDemo. Create two subclasses of UIViewController
, named PieVC and PizzaVC. For each, make sure to make the language Swift, and check the mark to make a Xib file for an iPhone like this:
Click open the AppDelegate.swift
file. Replace the class with the following:
class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let tabBarController = UITabBarController() let myVC1 = PieVC(nibName: "PieVC", bundle: nil) let myVC2 = PizzaVC(nibName: "PizzaVC", bundle: nil) let controllers = [myVC1,myVC2] tabBarController.viewControllers = controllers window?.rootViewController = tabBarController let firstImage = UIImage(named: "pie bar icon") let secondImage = UIImage(named: "pizza bar icon") myVC1.tabBarItem = UITabBarItem( title: "Pie", image: firstImage, tag: 1) myVC2.tabBarItem = UITabBarItem( title: "Pizza", image: secondImage, tag:2) return true } }
Let’s look at this code part by part. First we have this:
let tabBarController = UITabBarController() let myVC1 = PieVC(nibName: "PieVC", bundle: nil) let myVC2 = PizzaVC(nibName: "PizzaVC", bundle: nil)
Creates an instance of a UITabBarController
. We then create two view controllers using the xibs as our user interface. In full accordance with the adage “Life is uncertain, eat dessert first”, we’ll make the first page on launch be the pie page.
Let’s look at the next three lines:
let controllers = [myVC1,myVC2] tabBarController.viewControllers = controllers window?.rootViewController = tabBarController
TabBarControllers have a property called viewControllers
, which is an array of the view controllers in the order displayed in the tab bar. Our first line of this segment makes an array of view controllers in the order we want them to present. Next we assign the array to the tab bar controller. The last line assigns the controller as the root view controller.
We have out controller set up, but no titles or icons. The rest of this code assigns the title and image.
let firstImage = UIImage(named: "pie bar icon") let secondImage = UIImage(named: "pizza bar icon") myVC1.tabBarItem = UITabBarItem( title: "Pie", image: firstImage, tag: 1) myVC2.tabBarItem = UITabBarItem( title: "Pizza", image: secondImage, tag:2)
We have not loaded the images yet. You can download these images in this file.BarIcons.zip. Unzip the file and then place the small and the @2x pie bar image in the Assets.xcassets folder.
It should show up in the asset library as one set of images
Repeat this for the pizza icons. For more on creating the icons, see the storyboard version of this post.
Our next step is to set up the xibs. Click the pizzaVC.xib file, change the background color of the Xib to red, and place a label with a white background in the lower left tiled Pizza. Leave enough space on the bottom for the tab bar.
If you wish you can pin this label 50 left 0 right 100 bottom and 64 high, remembering to update the constraints.
Do the same for the PieVC.xib file, making the background green, and the label with a white background Pie. Use the Same constraints if you plan to use autolayout.
That is all you need to do. Build and run, and you will get two view controllers you can switch between.
Passing Data between View Controllers
While you can use delegates between view controllers in tab bar controllers, it’s debatable if you want to. Tab bar controllers break down MVC a bit. We use Tab bars for two conditions: when the view controllers are completely independent of each other and when they share the same model. Apple’s clock app is an example of a completely independent application. 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. We do not share anything and essentially have multiple apps running on different tabs, though with a particular theme.
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 for example.
View Controllers on a tab bar controller, unlike navigation controllers or Modal views, exist parallel to each other in to the array viewControllers
. When they disappear from the screen, they are not destroyed or made inactive. When they appear, they are not loaded. There are several ways to share a model between controllers. One popular which is very dangerous and should never be used is share the model in the app delegate. This is a global variable, which for safety and security purposes should be avoided. Another is to access the view controller through the tabBarControllers
property, which is the array of view controllers. This can get complicated quickly, since you need the subclass of UIViewController
. The best way is to make a subclass of UITabBarController
with the model as a property of the subclass. This way all the tab bar controllers share that same model, and all update the same model, but nothing else can touch the model.
Let’s make a simple model for another pizza and pie ordering app. In Xcode, press Command-N Create a new Cocoa Touch Class subclassing NSObject
called OrderModel. Change the code for OrderModel
to look like this:
import UIKit 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" } }
We have two properties and one method in this model. We’ll store an order for one pizza and one pie, and have a method to return what our order is.
Our next step is to subclass UITabBarController
. Under the OrderModel
class add the following class
class OrderTabBarController:UITabBarController{ let order = OrderModel() }
We have added one property to the tab bar controller, which is our model. In the app delegate change this line
let tabBarController = UITabBarController()
to this:
let tabBarController = OrderTabBarController() //Tab bar controller with model
Go into the PizzaVC
and clean out the code there. I removed all the regular template stuff for clarity and brevity.
class PizzaVC: UIViewController { }
View controllers have a property tabBarController
. When a view controller is connected to a UITabBarController
, this property points back to the Tab Bar controller. Go to the PizzaVC
class and add this code just after the class declaration:
private var tbvc = OrderTabBarController() private var myOrder = OrderModel()
Add viewDidLoad
to the class:
override func viewDidLoad() { super.viewDidLoad() //get the reference to the shared model tbvc = tabBarController as! OrderTabBarController myOrder = tbvc.order }
While we could have done this with one line and property, for clarity I did it in two. tbvc
gets the table view controller and down casts it to OrderTabBarController
. The property myOrder
gets the pointer to the model in the Tab controller. Notice I made sure nothing external has access to these two properties and made both private
We have our order model. Add an outlet and action this code to our PizzaVC
class.
@IBOutlet weak var orderLabel: UILabel! @IBAction func orderButton(sender: UIButton) { myOrder.pizza = (sender.titleLabel?.text)! orderLabel.text = myOrder.currentOrder() }
When we press a button, the title of the button will be the pizza order. We then use the currentOrder
method to give us our complete order.
Since viewDidLoad
only loads once per view controller in a tab controller, it is not where we want to update a view when returning from another tab view. Instead we need viewWillAppear
. Add this code:
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) orderLabel.text = myOrder.currentOrder() }
Our label will update whenever this view appears.
Let’s code the pie. Change the PieVC.swift
file as follows:
class PieVC: UIViewController{ private var myOrder = OrderModel() private var tbvc = OrderTabBarController() @IBOutlet weak var orderLabel: UILabel! override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) orderLabel.text = myOrder.currentOrder() } override func viewDidLoad() { super.viewDidLoad() //get the reference to the shared model tbvc = tabBarController as! OrderTabBarController myOrder = tbvc.order } }
We start with almost identical code to the Pizza’s code. This time we’ll use a picker for our pie choices. If you are interested in UIPickerView
, see my lesson on Implementing UIPickerView for details of what we are doing here. Add the delegate and data source to the class declaration.
class PieVC: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
You’ll get an error, which we will deal with later. Under the class declaration’s bracket, add the outlet and selection data
let selections = [ "Apple", "Cherry", "Banana Cream", "Key Lime", ] @IBOutlet weak var piePicker: UIPickerView!
In viewDidLoad
add the delegate and data source
piePicker.delegate = self piePicker.dataSource = self
Then implement the delegate and data source, which should remove the error on the class declaration.
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return selections.count } func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1 } func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return selections[row] } func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { myOrder.pie = selections[row] orderLabel.text = myOrder.currentOrder() }
Most of the code above runs the picker. The key two lines of code are similar to what we used in the button on PizzaVC
myOrder.pie = selections[row] orderLabel.text = myOrder.currentOrder()
The first line here takes the selection and makes it the value of the pie
property. The second updates our display using the currentOrder
method.
Laying out the Xibs
Now that we have code go to the PizzaVC.xib file. Add two or more buttons and a label to the xib, all with white backgrounds.
Title one button Cheese and one Pepperoni I used autolayout to make it look pretty but that is up to you. For those using autolayout, I pinned the label 30 points to the top margin and the buttons 10 points up to whatever is above it. All these views get pinned 50 points to the left and 0 points to the right. I also made everything 44 points high.
Open the assistant editor and hook up the buttons to the IBAction
. Hook up the label to the orderlabel
outlet.
Go to the pieVC.xib file and add a label and a picker view. Again I used autolayout first pinning the label to the top the same as I did for the pizza. I added the picker view between the two labels, and then pinned the picker 20 points on all sides.
Using the assistant editor, hook up the picker to the piePicker
outlet and the label to the orderLabel
outlet.
Build and run. Switch between the two tabs and change the data.
This covers the basics of programmatic Tab bar controllers. To be clear, I don’t use the programmatic version, because the storyboard versions are a lot easier to use and more powerful right out of the box. For more on Storyboard tab bar controllers, look at this post
The Whole Code
AppDelegate.swift
// // AppDelegate.swift // TabBarDemo // // Created by Steven Lipton on 9/20/15. // Copyright © 2015 MakeAppPie.Com. All rights reserved. // New version for Swift 2.0/iOS9 import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let tabBarController = OrderTabBarController() //Tab bar controller with model //let tabBarController = UITabBarController() let myVC1 = PieVC(nibName: "PieVC", bundle: nil) let myVC2 = PizzaVC(nibName: "PizzaVC", bundle: nil) let controllers = [myVC1,myVC2] tabBarController.viewControllers = controllers window?.rootViewController = tabBarController let firstImage = UIImage(named: "pie bar icon") let secondImage = UIImage(named: "pizza bar icon") myVC1.tabBarItem = UITabBarItem( title: "Pie", image: firstImage, tag: 1) myVC2.tabBarItem = UITabBarItem( title: "Pizza", image: secondImage, tag:2) return true } }
OrderModel.Swift
// // OrderModel.swift // TabBarDemo // // Created by Steven Lipton on 9/20/15. // Copyright © 2015 MakeAppPie.Com. All rights reserved. // New Version for Swift 2.0/iOS9 import UIKit 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" } } class OrderTabBarController:UITabBarController{ let order = OrderModel() }
PieVC.swift
// // PieVC.swift // TabBarDemo // // Created by Steven Lipton on 9/20/15. // Copyright © 2015 MakeAppPie.Com. All rights reserved. // New Version for Swift 2.0/iOS9 import UIKit class PieVC: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { let selections = [ "Apple", "Cherry", "Banana Cream", "Key Lime", ] @IBOutlet weak var piePicker: UIPickerView! private var myOrder = OrderModel() private var tbvc = OrderTabBarController() @IBOutlet weak var orderLabel: UILabel! override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) orderLabel.text = myOrder.currentOrder() } override func viewDidLoad() { super.viewDidLoad() //get the reference to the shared model tbvc = tabBarController as! OrderTabBarController myOrder = tbvc.order piePicker.delegate = self piePicker.dataSource = self } //MARK: Delegates and Data sources func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return selections.count } func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return 1 } func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return selections[row] } func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { myOrder.pie = selections[row] orderLabel.text = myOrder.currentOrder() } }
PizzaVC.Swift
// // PizzaVC.swift // TabBarDemo // // Created by Steven Lipton on 9/20/15. // Copyright © 2015 MakeAppPie.Com. All rights reserved. // New Version for Swift 2.0/iOS9 import UIKit class PizzaVC: UIViewController { private var myOrder = OrderModel() private var tbvc = OrderTabBarController() @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() //get the reference to the shared model tbvc = tabBarController as! OrderTabBarController myOrder = tbvc.order } }
Leave a Reply