[Converted to Swift 2.0 — SJL 9/17/15 ]
Navigation controllers are the workhorse of organizing view controllers. I’ve covered much of their use in other posts about MVC, segues and delegates. In this chapter, We’ll go through some of the Swift code for the Navigation controller.
The View Controller Stack
Navigation view controllers are stack based. The newest view controller is the visible one. It is on top of the last one we saw.
If you are not familiar with the stack data structure, it is useful to understand stacks and their nomenclature a little better. Stacks work a lot like a deck of cards that are face up. You can only see the top card. When you place a card on top of the stack, you push a card on the stack. When you remove a card from the stack and show the card below it, you pop it off the stack. We use the terms push and pop a lot to talk about stacks, and you will find they describes methods often.
Opening a View Controller in a Xib
I’ve shown elsewhere how to move to navigation controllers through segues. Let’s look at a few ways to do so programmatically by pushing and popping to the navigation stack directly.
Start a new single view project in Swift called SwiftProgNavControllerDemo
. Go into the storyboard and select the blank view controller. Be sure to select the controller and not the view. From the drop down menu select Edit>Embed in > Navigation Controller.
In the view controller, Add a label and a button so your code looks like the diagram:
Open the assistant editor. Control-drag the button and make an action for a UIButton
called nextButton
. Remove the commented out method except the viewDidLoad()
. Add the following two lines to the nextButton
let vc = TwoViewController(nibName: "TwoViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true)
Line 1 creates a view controller of class TwoViewController
, using the XIB of the same name. Line 2 pushed the view controller on the navigation controller stack maintained by ViewController
. Your code should look like this when done:
class ViewController: UIViewController { @IBAction func nextButton(sender: UIButton) { let vc = TwoViewController(nibName: "TwoViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, // typically from a nib. } }
Close the assistant editor.
We need another view controller as a destination. Press Command-n or click File>New>File… Choose a iOS source template of Cocoa Touch Class. Make the new file a subclass of UIViewController
and name the file TwoViewController. To prove we are doing nothing with the storyboard, Click the option Also create XIB file to yes.
You will find a new XIB in interface builder. Set it up to look like the illustration below.
Open the assistant editor and control-drag the back button inside the TwoViewController
class. Make an @IBAction
method named backButton
as an UIButton
. Do the same for the Next button, but label the action method nextButton
as we did in the last view controller. Add the following code to the nextButton()
method:
let vc = ThreeViewController(nibName: "ThreeViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true )
Your code should look like this:
class TwoViewController: UIViewController { @IBAction func nextButton(sender: UIButton) { let vc = ThreeViewController(nibName: "ThreeViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true ) } @IBAction func backButton(sender: AnyObject) { } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup // after loading the view. } }
Let’s do this one more time so we end up with three view controllers to push onto the view controller stack. Follow the same procedure as you did for TwoViewController
, but name it ThreeViewController
, Instead of a Next button make a button Root like the diagram shows.
Control-drag the buttons to make two @IBAction
methods named backButton
and rootButton
. Once again, remove the extra code except viewDidLoad()
. Your ThreeViewController
class should look like this:
class ThreeViewController: UIViewController { @IBAction func rootButton(sender: UIButton) { } @IBAction func backButton(sender: UIButton) { } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } }
Now that we have set up the views, build and run. Tap the Next button and move between the three view controllers. Pushing a view controller in these controllers is a mere two lines of code:
let vc = ViewControllerName(nibName: "nameOfNib", bundle: nil) navigationController?.pushViewController(vc, animated: true )
The first line creates the view controller, which of course we can name whatever we want. I tend to keep it simple and use vc
, though if I had more than one, I’d be more descriptive. We used xibs here, which work a lot better than trying to connect to views on a storyboard. There’s a bunch of weak variables you have to dance over in that case, so often it just doesn’t work.
I rarely push view controllers. there are situations where you cannot use the storyboard and this is the alternative. I prefer the storyboard for two reasons: first it is better documentation of my user interface. Secondly, I prefer to let the system do as much of the background work as possible. The deeper into code you go, the more you have to worry about unexpected bugs. Swift and ARC Together often set nil
when you don’t expect or want it. Crashing is a good way to catch those in early development, so you don’t have bigger more subtle bugs later.
Manual Segues to View Controllers
In other discussions of Segues, we talked about direct segues, connecting up a button directly to a view. You can do segues programmatically, which is useful when you conditionally go to a view. Now add another subclass of UIViewController
called FourViewController.
This time you do not need a XIB. Go into the storyboard and drag a view controller onto the storyboard. Click on its view controller icon. In the identity inspector make the custom class FourViewController
. Add a label to the view just so you know it is there.
Click on the ViewController
scene title in the storyboard. From the view controller Icon on ViewController
, control-drag from ViewController
to somewhere on FourViewController
’s content, and release
the mouse button. In the popup, select Show. Go into the properties inspector and set the segue’s identifier to four.
Drag another button out to the View Controller scene and make the title Four
. Go to ViewController
class and add the following method:
@IBAction func fourButton(sender: UIButton){ performSegueWithIdentifier("four", sender: self) }
Open the assistant editor and drag from the circle next to the fourButton()
method over the Four button and release.
The code above is a mere one line: it runs the segue. If you set the segue identifier correctly, that is all you need to do.
Where this is useful is conditional cases. We may want to go to different view controllers based on conditions in the current view controller or model. Let’s try an example: Drag another view controller out to the storyboard. Set it up just like FourViewController
except call it FiveViewController
.
Add a label with the text View Controller Five in the new scene. Make a segue with an identifier five by control-dragging from ViewController
to FiveViewController
. Add a switch to the content view as in the illustration,
Hook up an outlet by control dragging from the switch to the ViewController
class in the assistant editor.
@IBOutlet weak var fourFiveSwitch: UISwitch! Change the code for the fourButton() method to the following: @IBAction func fourButton(sender: UIButton){ if fourFiveSwitch.on{ performSegueWithIdentifier("four", sender: self) }else{ performSegueWithIdentifier("five", sender: self) } }
Build and run. Tap the four button and then hit the back button in the navigation bar. Now change the switch setting and tap the Four again.
Closing a View Controller
Up to now, we’ve relied on the navigation controller’s back button. While I may not use pushViewController()
a lot, I do use popViewController()
often. Almost every delegate between view controllers in a navigation stack will use it. There are several versions of the controller and I wanted to explore them with you.
In the TwoViewController
class, change the backButton
code to read:
@IBAction func backButton(sender: UIButton) { navigationController?.popViewControllerAnimated(true) }
Now do the same in ThreeViewController:
@IBAction func backButton(sender: UIButton) { navigationController?.popViewControllerAnimated(true) }
Also in ThreeViewController
, Let’s add to the rootButton()
method the following.
@IBAction func rootButton(sender: UIButton) { navigationController?.popToRootViewControllerAnimated(true) }
Build and Run. you will find the Next and Root button now take you around the application.
There are three versions of pop: popViewController()
, popToRootController()
, and popToViewController()
The most Common is popViewController()
which removes the top view controller from the stack. popToRootViewController()
and popToViewController()
pops everything or everything up to a specific View Controller off the stack, returning what it popped off.
Moving Values
So far we have not moved values between view controllers. If using a storyboard and segues with a performSegueWithIdentifier()
, It is the same way we have already talked about for segues and storyboard. If we don’t use segues, it is rather simple, Be careful how you use it, since it is easy to break MVC.
Let’s start by changing the view controller class we have written to this:
class ViewController: UIViewController { var vcCount = 0 @IBAction func nextButton(sender: UIButton) { let vc = TwoViewController(nibName: "TwoViewController", bundle: nil) vc.vcCount = vcCount++ navigationController?.pushViewController(vc, animated: true) } @IBOutlet weak var fourFiveSwitch: UISwitch! @IBAction func fourButton(sender: UIButton){ if fourFiveSwitch.on{ performSegueWithIdentifier("four", sender: self) }else{ performSegueWithIdentifier("five", sender: self) } } }
We assign an integer property vcCount
a value of zero in line 2. Line 5 increments the count, then sends that to the property of the TwoViewController
instance we created in line 4. Change TwoViewController
to this:
class TwoViewController: UIViewController { var vcCount:Int = 0 @IBAction func nextButton(sender: UIButton) { let vc = ThreeViewController(nibName: "ThreeViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true ) } @IBAction func backButton(sender: UIButton) { navigationController?.popViewControllerAnimated(true) } override func viewDidLoad() { super.viewDidLoad() print("\(vcCount) ") } }
On line 2, we added a integer property vcCount
. In viewDidLoad()
, we printed the property to the console.
Using the Delegate
We can send data to other controllers now, but we need delegates to return it from a popped controller to the next visible controller. The process is the same as discussed in my delegates post, except there is no prepareForSegue()
In the ViewTwoController.swift
file, make a protocol
protocol TwoVCDelegate{ func didFinishTwoVC(controller:TwoViewController) }
We will need to add the delegate to TwoViewController
:
var delegate:TwoVCDelegate! = nil
We’ll use the protocol by changing our backButton
method in TwoViewController
to:
@IBAction func backButton(sender: UIButton) { //navigationController?.popViewControllerAnimated(true) delegate.didFinishTwoVC(self) }
Our final steps are to adopt the protocol in ViewController
, by changing the code like this:
class ViewController: UIViewController,TwoVCDelegate { var vcCount:Int = 0 func didFinishTwoVC(controller: TwoViewController) { vcCount = controller.vcCount + 1 controller.navigationController?.popViewControllerAnimated(true) } @IBAction func nextButton(sender: UIButton) { let vc = TwoViewController(nibName: "TwoViewController", bundle: nil) vc.vcCount = vcCount vc.delegate = self navigationController?.pushViewController(vc, animated: true) } @IBOutlet weak var fourFiveSwitch: UISwitch! @IBAction func fourButton(sender: UIButton){ if fourFiveSwitch.on{ performSegueWithIdentifier("four", sender: self) }else{ performSegueWithIdentifier("five", sender: self) } } }
Line 1 adopts the delegate, and lines 3-6 is the required method didFinishTwoVC
. In that method we take the current value in the vcCount
property in TwoViewController
and increment it, then pop the controller off the stack. In line 10, we added the assignment of the TwoViewController
‘s delegate to our current view controller.
One More Thing: Setting the Navigation Title Bar
We have been using the console to tell us the value of vcCount
in the view controllers. We can also place it in the title of the navigation bar. Change the viewDidLoad()
in TwoViewController
to this:
override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Count: \(vcCount)" }
All you need to do to change the navigation bar title is assign a string to navigationItem.title
. The navigationItem
is a property of type UINavigationItem
, and has a property to quickly change the title of the bar. There is a lot you can change in the UINavigationItem
, but that is a topic for another post.
Also change the vcCount
declaration to this in ViewController
:
var vcCount:Int = 0{ didSet{ navigationItem.title = "Count: \(vcCount)" } }
We cannot use viewDidload()
in this case since the root view controller will load only once. We could have used viewWillAppear()
instead, but we can also use the property observer feature of Swift. Using didSet
, any time vcCount
changes, the title changes with it. Build and run. Go back and forth from view one to view two.
The Whole Code
// // ViewController.swift // SwiftNavControllerDemo // // Created by Steven Lipton on 9/10/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class ViewController: UIViewController,TwoVCDelegate { var vcCount:Int = 0{ didSet{ navigationItem.title = "Count: \(vcCount)" } } func didFinishTwoVC(controller: TwoViewController) { vcCount = controller.vcCount + 1 controller.navigationController?.popViewControllerAnimated(true) } @IBAction func nextButton(sender: UIButton) { let vc = TwoViewController(nibName: "TwoViewController", bundle: nil) vc.vcCount = vcCount vc.delegate = self navigationController?.pushViewController(vc, animated: true) } @IBOutlet weak var fourFiveSwitch: UISwitch! @IBAction func fourButton(sender: UIButton){ if fourFiveSwitch.on{ performSegueWithIdentifier("four", sender: self) }else{ performSegueWithIdentifier("five", sender: self) } } } // // TwoViewController.swift // SwiftNavControllerDemo // // Created by Steven Lipton on 9/10/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit protocol TwoVCDelegate{ func didFinishTwoVC(controller:TwoViewController) } class TwoViewController: UIViewController { var vcCount:Int = 0 @IBAction func nextButton(sender: UIButton) { let vc = ThreeViewController(nibName: "ThreeViewController", bundle: nil) navigationController?.pushViewController(vc, animated: true ) } var delegate:TwoVCDelegate!=nil @IBAction func backButton(sender: UIButton) { //navigationController?.popViewControllerAnimated(true) delegate.didFinishTwoVC(self) } override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Count: \(vcCount)" } } // // ThreeViewController.swift // SwiftNavControllerDemo // // Created by Steven Lipton on 9/11/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class ThreeViewController: UIViewController { @IBAction func rootButton(sender: UIButton) { navigationController?.popToRootViewControllerAnimated(true) } @IBAction func backButton(sender: UIButton) { navigationController?.popViewControllerAnimated(true) } override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Three" // Do any additional setup after loading the view. } } // // FourViewController.swift // SwiftNavControllerDemo // // Created by Steven Lipton on 9/11/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class FourViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Four" //another example of a bar title } } // // FiveViewController.swift // SwiftNavControllerDemo // // Created by Steven Lipton on 9/11/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class FiveViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Five" //another example of a bar title } }
Leave a Reply