Codepath

Navigation Controller

Updated with iOS 10.3, Xcode 8.3.3 and Swift 3

Overview

In a typical iOS project you will create one view controller for each "screen" in your application. This means you need a way to coordinate the interaction of these view controllers and the presentation of their associated views.

In this lesson we will show you how to

  • Take the user to another screen (view controllers) within your application
  • Pass data between view controllers
  • Create a navigation flow with or without a storyboard

Navigation Controller

A common way to manage a group of view controllers that are associated with a hierarchy of content is to use a UINavigationController. Navigation controllers maintain a stack of view controllers that are pushed onto and popped off of the top of the stack as the user browses through the hierarchy. Their behavior is somewhat similar to that of a web browser's history and "back button" functionality.

The navigation controller is one example of a built-in UIKit container view controller. An in-depth guide by Apple covering navigation controllers and all other built-in container view controllers can be found here.

Basic use of the navigation controller

This guide covers the most common use cases for navigation controllers and how to pass data between view controllers managed by a navigation controller. To demonstrate this functionality, we will build a demo application that presents the user with a table of names. When the user selects a name in the table we will show a detail view controller that allows the user to edit the name.

The process for using navigation controllers in storyboard applications actually quite different than when using them programmatically since the concept of a segue does not exist outside the storyboard. We'll create the example first with storyboards. Then we'll show how the same example would be built without using storyboards.

Using navigation controllers in storyboards

First we'll set up our table to show the list of names. Starting with the Xcode Single View Application template. We add a table view to the initial view controller that Xcode has generated and add a single prototype cell that just uses the built-in UITableViewCell class. We hook up our Table View's dataSource property to finish the initial setup. If you are unfamiliar with table views please refer to the table view guide.

import UIKit

class ViewController: UIViewController, UITableViewDataSource {

    let names = ["Brent Berg", "Cody Preston", "Kareem Dixon", "Xander Clark",
        "Francis Frederick", "Carson Hopkins", "Anthony Nguyen", "Dean Franklin",
        "Jeremy Davenport", "Rigel Bradford", "John Ball", "Zachery Norman",
        "Valentine Lindsey", "Slade Thornton", "Jelani Dickson", "Vance Hurley",
        "Wayne Ellison", "Kasimir Mueller", "Emery Pruitt", "Lucius Lawrence",
        "Kenneth Mendez"]

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        tableView.dataSource = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return names.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "NameCell", for: indexPath)
        cell.textLabel?.text = names[indexPath.row]
        return cell
    }
}

Adding the navigation controller and setting the root view controller

Next we'll need to add a navigation controller. When we instantiate a navigation controller, we must also set its root view controller. This is the first view controller that will be shown when the navigation controller is loaded. Think of it as the "home page" in our web browser analogy.

One way to add a navigation controller is by dragging a Navigation Controller from the object library into our storyboard. You'll notice that if you do this, Xcode will automatically create a separate view controller to act as the root view controller. We'll want to set the root view controller to the one with our table and delete the other one. This can be done by control dragging from the navigation controller to our view controller and selecting the root view controller Relationship Segue. If you haven't worked with segues before, a segue (pronounced "seg-way") is a storyboard's representation of a relationship or transition between different view controllers. We'll also need to set the initial view controller to the navigation controller by dragging the arrow that was pointing to the view controller with our table.

In most cases if we have a view controller already set up that we want to act as our root view controller, we can accomplish all the above steps simply by selecting the menu item Editor -> Embed In -> Navigation Controller. Running the app, we can see our navigation controller's translucent grey navigation bar (or a gap of space above the first name in the list) at the top of the screen.

Pushing a view controller onto the navigation stack

We'll need to create another view controller class to display and allow the user to edit an individual name. We do this by dragging another view controller from the Object Library onto our storyboard. We add two text fields to this view controller to store the first and last parts of the name the user will be editing. We create a new subclass of UIViewController called NameController and set the new view controller's Custom Class property to be NameController in our storyboard. Once the custom class is set, we can go ahead and create outlets in our NameController class corresponding to the two text fields. Note: we recommend pressing Enter or Tab after entering the class value since Xcode may not apply the change if you navigate out of the text field otherwise.

import UIKit

class NameController: UIViewController {

    @IBOutlet weak var firstNameTextField: UITextField!
    @IBOutlet weak var lastNameTextField: UITextField!
}

When the user selects name by tapping on a row in the table, we want to display the NameController by pushing it onto the navigation stack. This can be done by control-dragging from the prototype cell to our name controller and selecting show under Selection Segue. Once this is done, notice how our name controller now also shows the grey navigation bar to indicate that it will be shown as part of the navigation stack.

It is important that we select the show segue since this corresponds to the navigation controller pushing a new view controller onto the stack. The other segues correspond to other ways of transitioning to a new view controller including presenting it modally.

Since we initiated our control-drag from the prototype cell, the Selection Segue group was available. Selecting a segue under this group means "respond to the selection event with this transition." Other events could be available depending on the control that initiates the segue. For example control-dragging from a button will show possible Action Segues. Running our application right now shows the following.

Configuring a view controller before it is pushed

You'll notice at least one problem immediately with the current behavior: the name we selected is not populated in first and last name fields of our NameController. We'll need to configure the name controller before it gets displayed on the screen. We do this by first providing an identifier to our selection segue. Next we write some code in our root view controller's prepareForSegue method to respond should a segue with this identifier be triggered.

We give the segue an identifier by selecting the storyboard and using the Attributes Inspector. Notice that our prototype cell is also highlighted to indicate that segue will be trigged by selecting a cell.

In our root view controller's prepareForSegue method, we check to see if the segue that was triggered matches the identifier we gave our segue in the storyboard. If it does we know that the segue was trigged in response a user selecting a cell, and thus the sender parameter will be the cell. If for example the segue had been triggered by a button tap, the sender would be the button.

The segue parameter of prepareForSegue gives us access to the destinationViewController so that we can configure the new view controller before it is displayed. In this case we know that for this particular segue the destinationViewController will be a NameController. We'll have to update our NameController to have the ability to set the name and have it displayed properly when the view is loaded. Finally, we also deselect the row since we won't want it to be selected once we go back from the name controller.

class ViewController: UIViewController, UITableViewDataSource {
    ...
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showNameControllerSegue" {
            let cell = sender as! UITableViewCell
            if let indexPath = tableView.indexPath(for: cell) {
                let nameController = segue.destination as! NameController
                nameController.fullName = names[indexPath.row]
                tableView.deselectRow(at: indexPath, animated: true)
            }
        }
    }
 }
class NameController: UIViewController {
    @IBOutlet weak var firstNameTextField: UITextField!
    @IBOutlet weak var lastNameTextField: UITextField!

    var fullName: String?

    override func viewDidLoad() {
        if let fullName = self.fullName {
            let firstLast = fullName.components(separatedBy: " ")
            firstNameTextField.text = firstLast[0]
            lastNameTextField.text = firstLast[1]
        }
    }
}

Unwind segues and passing information back up the hierarchy

Next we'll need a way for the user to save any changes to a name back to our table. We'll do this by making use of an unwind segue. An unwind segue is a way for a view controller to respond to an event by navigating back to the view controller that caused it to be loaded. In the the context of navigation controllers, this means the view controller that is one below the current view controller on the navigation stack. You can read more about on Apple Developer about Dismissing a view controller with an unwind segue and Using Unwind Segues.

First off, we'll need a way for the user to indicate they want to save the change. We can add a button to the right side of the navigation bar in our name controller by dragging a Navigation Item and then a Bar Button Item from the Object Library.

To implement an unwind segue we create an @IBAction method in the view controller that will be unwound to taking a single UIStoryboardSegue parameter. In our case this means we added the below saveName method to our root view controller.

class ViewController: UIViewController, UITableViewDataSource {
    ...
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showNameControllerSegue" {
            let cell = sender as! UITableViewCell
            if let indexPath = tableView.indexPath(for: cell) {
                let nameController = segue.destination as! NameController
                nameController.fullName = names[indexPath.row]
                tableView.deselectRowAtIndexPath(indexPath, animated: true)
            }
        }
    }

    @IBAction func saveName(segue: UIStoryboardSegue) {
        // add logic here to handle a transition back from the
        // name controller resulting from a user tapping on Save
    }
}

Next, we'll add an unwind segue by control-dragging our Save Name scene icon to the Exit icon, and selecting the @IBAction saveButton segue we just created. Afterwards we'll have to hook up our Save button to this segue by control-dragging from the button to the Exit outlet and selecting the same @IBAction we just created. The Exit outlet is the red exit door that appears above of your active view controller in the storyboard.

Finally we'll need to write the code in our root view controller to handle updating the name once the saveName method is called.

class ViewController: UIViewController, UITableViewDataSource {

    var names = ["Brent Berg", "Cody Preston", "Kareem Dixon", "Xander Clark",
        "Francis Frederick", "Carson Hopkins", "Anthony Nguyen", "Dean Franklin",
        "Jeremy Davenport", "Rigel Bradford", "John Ball", "Zachery Norman",
        "Valentine Lindsey", "Slade Thornton", "Jelani Dickson", "Vance Hurley",
        "Wayne Ellison", "Kasimir Mueller", "Emery Pruitt", "Lucius Lawrence",
        "Kenneth Mendez"]

    var selectedIndexPath: IndexPath?

    @IBOutlet weak var tableView: UITableView!

    ...

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showNameControllerSegue" {
            let cell = sender as! UITableViewCell
            if let indexPath = tableView.indexPath(for: cell) {
                let namesController = segue.destination as! NameController
                namesController.fullName = names[indexPath.row]
                tableView.deselectRowAtIndexPath(indexPath, animated: true)
                selectedIndexPath = indexPath
            }
        }
    }

    @IBAction func saveName(segue: UIStoryboardSegue) {
        let nameController = segue.source as! NameController
        let name = nameController.firstNameTextField.text! + " " + nameController.lastNameTextField.text!

        if let indexPath = selectedIndexPath {
            names[indexPath.row] = name
            tableView.reloadRows(at: [indexPath], with: .automatic)
            selectedIndexPath = nil
        }
    }
}

Notice that we were able to obtain a reference to the name controller in saveName by using segue.sourceViewController. This makes sense because the name controller is the source of the unwind segue, whereas the destination controller is the root view controller. The other thing to note is that we had to maintain an additional state variable selectedIndexPath to help us remember which name it was we were editing. Putting everything together we get the following functionality:

Using navigation controllers without storyboards

We will now show how the same example would be built in a non-storyboard application. Conceptually, some of the steps will be the same. However, since segues do not exist outside a storyboard, we will have to set up more code ourselves to handle events and interface between view controllers.

As above we'll first set up our table to show the list of names. We'll start again with the Xcode Single View Application template. Xcode 6 and above will automatically create a Main.storyboard file and include it in our build settings. We will go ahead and remove this file and update Info.plist file (under Supporting Files) to remove the reference to it by clicking on the minus sign after selecting Main storyboard file base name.

Without the storyboard, we'll have to set up our window and root view controller manually by changing the didFinishLaunchingWithOptions method in our AppDelegate. More on the AppDelegate can be found in the basic application architecture guide.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = ViewController()
        window?.makeKeyAndVisible()
        return true
    }
    ...
}
Notice that we made use of the main ViewController class that Xcode generated for us. Now we can add our table of names to this view controller. We do everything programmatically here, but we could have just as easily set up a separate .xib file for this view controller. If you are unfamiliar with table views please refer to the table view guide.
import UIKit

class ViewController: UIViewController, UITableViewDataSource {
    let names = ["Brent Berg", "Cody Preston", "Kareem Dixon", "Xander Clark",
        "Francis Frederick", "Carson Hopkins", "Anthony Nguyen", "Dean Franklin",
        "Jeremy Davenport", "Rigel Bradford", "John Ball", "Zachery Norman",
        "Valentine Lindsey", "Slade Thornton", "Jelani Dickson", "Vance Hurley",
        "Wayne Ellison", "Kasimir Mueller", "Emery Pruitt", "Lucius Lawrence",
        "Kenneth Mendez"]

    var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView = UITableView(frame: view.frame)
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "NameCell")
        view.addSubview(tableView)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return names.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "NameCell", for: indexPath)
        cell.textLabel?.text = names[indexPath.row]
        return cell
    }
}

Running the app at this point gives us:

Adding the navigation controller and setting its root view controller

Next we'll need to add a navigation controller and set its root view controller—the first view controller that will be shown when the navigation controller is loaded—to be our main view controller with the table.

We can do this all in code by simply instantiating a UINavigationController and setting its properties. Since we want the navigation controller to be the first view controller shown when the app is loaded, we can do this in our AppDelegate's didFinishLaunchingWithOptions method where we also set the application window's root view controller to be the navigation controller.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let mainVC = ViewController()
        let navigationVC = UINavigationController(rootViewController: mainVC)
        window?.rootViewController = navigationVC
        window?.makeKeyAndVisible()
        return true
    }
    ...
}

Running the application at this point will show the same table with a grey navigation bar at the top.

Initializing and pushing a view controller onto the stack

As in our storyboard example, we'll need to create another view controller class to display and allow the user to edit an individual name. Instead of laying out our view programmatically as we did in the main ViewController class, we'll create new class NameController and associated .xib by selecting File -> New -> File... -> iOS -> Source -> Cocoa Touch Class. Be sure to tick in the Also Create XIB file.

NB: Most of the time you'll want to be consistent and use only one method of laying out your views (programmatic, separate .xibs, or have your app entirely within storyboards). Here we use both .xib and programmatic layout to emphasize the point that anything outside a storyboard cannot use segues and thus must programmatically manipulate the navigation view controller.

We can now open up NameController.xib in Interface Builder. As before, we'll add two text fields to this view controller to store the first and last parts of the name the user will be editing. Using the Assistant Editor (tuxedo) view we also create outlets in our NameController class corresponding to the two text fields by controlling dragging the fields into the code view.

We'll go ahead and add the code that will allow us to configure the NameController to with the fullName that it will display when loaded.

import UIKit

class NameController: UIViewController {

    @IBOutlet weak var lastNameTextField: UITextField!
    @IBOutlet weak var firstNameTextField: UITextField!

    var fullName: String?

    override func viewDidLoad() {
        if let fullName = self.fullName {
            let firstLast = fullName.components(separatedBy: " ")
            firstNameTextField.text = firstLast[0]
            lastNameTextField.text = firstLast[1]
        }
    }
}

NB: The following only affects Swift projects:

Previously when initializing a view controller class named Foo via the default (argumentless) initializer, UIKit will look for a matching Foo.xib and load the UI elements from this file. In the latest versions of Xcode Apple has changed this behavior (for Swift classes only) to look for an associated .xib named YourProjectName.Foo.xib.

There are a few ways to work around this behavior discussed here and here. In this example we simply renamed our NameController.xib file to NavigationNoStoryboard.NameController.xib where NavigationNoStoryboard was the the name of our project.

Remember that we want to show the NameController and add it to the navigation stack when the user selects a name in our main ViewController containing the table of names. By having our main ViewController implement the UITableViewDelegate protocol we have a convenient hook to respond to the row selection event by overriding the didSelectRowAtIndexPath method. Again, you should refer to the table view guide if you are unfamiliar with this aspect of table views.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView = UITableView(frame: view.frame)
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "NameCell")
        view.addSubview(tableView)
    }

    ...

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let nameVC = NameController()
        nameVC.fullName = names[indexPath.row]

        tableView.deselectRow(at: indexPath, animated: true)
        self.navigationController?.pushViewController(nameVC, animated: true)
    }
}

Notice that we are able to obtain a reference to the navigation controller via self.navigationController. This is set automatically for us by UIKit when any view controller comes onto the navigation stack. In this case, it happened when we set this view controller as our navigation controller's root view controller in the AppDelegate above.

We push nameVC onto the navigation stack and display it as the top view controller with pushViewController. After this call is made, the nameVC.navigationController property will also be automatically set to the same navigation controller.

NB: Technically in Swift, prefixing a property with self. is not required and is specifically recommended against by both github's and Ray Wenderlich's style guides. We include it here to emphasize that this is a built-in property on the view controller.

Running the application at this stage gives us the following behavior.

Passing information back up the stack with a delegate

We still need to implement the ability to save changes to a name in NameController back to the table view in the main ViewController. Since there's no such thing as an unwind segue without a storyboard, we'll have to implement our own mechanism for sending the new name back to the main ViewController.

The typical pattern to accomplish this in iOS programmatically is to use a delegate. What this means in this case is that the NameController needs to keep a reference to the main ViewController so that it can inform the main ViewController when the user has saved changes to a name and what the new name is.

However, since we defined our NameController outside of a storyboard, there is no way in general, to tell what view controller has instantiated it and pushed it onto the navigation stack (if there is even a navigation stack). For example, later on, we might want to reuse NameController to edit names a different situation where we instantiated it and presented it from a view controller class we haven't even created at this time.

Thus maintaining a reference to something of type ViewController would unnecessarily couple the ViewController and NameController classes. To solve this problem we can introduce a protocol (sometimes called interface in other programming environments) NameControllerDelegate to represent something that is able to respond events from NameController. We'll make our main ViewController implement NameControllerDelegate similar to how it implemented UITableViewDelegate in order to respond to events from the UITableView.

Our NameControllerDelegate protocol defines a single method didSaveName that will be called when the user has initiated a save action in our NameController. In order to call this method, we'll need to keep a reference to the delegate in our NameController.

Lastly, we need to add a save button to our navigation bar and set its action to call a method saveButtonTapped in our NameController that will propagate the event to our NameControllerDelegate.

After putting everything together the code looks like this:

import UIKit

protocol NameControllerDelegate: class {
    func nameController(nameVC: NameController, didSaveName name: String)
}

class NameController: UIViewController {

    @IBOutlet weak var lastNameTextField: UITextField!
    @IBOutlet weak var firstNameTextField: UITextField!

    weak var delegate: NameControllerDelegate?
    var fullName: String?

    override func viewDidLoad() {
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(NameController.saveButtonTapped)
        self.navigationItem.title = "Edit Name"

        if let fullName = self.fullName {
            let firstLast = fullName.components(separatedBy: " ")
            firstNameTextField.text = firstLast[0]
            lastNameTextField.text = firstLast[1]
        }
    }

    func saveButtonTapped() {
        let name = firstNameTextField.text + " " + lastNameTextField.text
        delegate?.nameController(self, didSaveName: name)
    }
}
import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NameControllerDelegate {
    var names = ["Brent Berg", "Cody Preston", "Kareem Dixon", "Xander Clark",
        "Francis Frederick", "Carson Hopkins", "Anthony Nguyen", "Dean Franklin",
        "Jeremy Davenport", "Rigel Bradford", "John Ball", "Zachery Norman",
        "Valentine Lindsey", "Slade Thornton", "Jelani Dickson", "Vance Hurley",
        "Wayne Ellison", "Kasimir Mueller", "Emery Pruitt", "Lucius Lawrence",
        "Kenneth Mendez"]

    var tableView: UITableView!
    var selectedIndexPath: IndexPath?

    ...

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let nameVC = NameController()
        nameVC.fullName = names[indexPath.row]
        nameVC.delegate = self

        selectedIndexPath = indexPath
        tableView.deselectRow(at: indexPath, animated: true)
        self.navigationController?.pushViewController(nameVC, animated: true)
    }

    func nameController(nameVC: NameController, didSaveName name: String) {
        if let indexPath = selectedIndexPath {
            names[indexPath.row] = name
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }
        self.navigationController?.popViewControllerAnimated(true)
    }
}

There are quite a few things to note in the above code.

  • We restrict that only class types (as opposed to structs) can implement our NameControllerDelegate protocol by including the class keyword in the declaration in the inherited protocols list after the colon. This is so we can declare our delegate property as a weak reference (since structs cannot be weak references). This point is language specific to Swift.

  • The reason we need our delegate property to be a weak reference is due to the fact that iOS manages (and frees) memory by using a technique called automatic reference counting (ARC). Since delegates commonly refer back to objects that instantiated them (or objects referred to by these objects), we'll always want to declare our delegate properties as weak in order to avoid strong reference cycles that lead to memory leaks. An extensive discussion of ARC can be found here.

  • The navigationItem property of NameController is used by the navigation controller to determine what to display in the navigation bar when NameController is on top of the navigation stack. This provides us with a convenient hook to customize the content of the navigation bar. More on this can be found below in discussion of customizing the navigation bar's appearance below.

  • When creating the Save button by instantiating a UIBarButtonItem we specified how to respond to the button being tapped with the target and action parameters. This is an example of target-action pattern (sometimes called target-selector). This invocation basically means "call the method identified by action on the target object when the button is tapped".

NB: In Objective-C the action parameter has to a be selector. In Swift, strings are automatically converted to selectors when necessary.

  • As before in our storyboard example, we need to maintain an additional state variable selectedIndexPath so that when the delegate method didSaveName is called, we can know which name to update.

  • Notice that we set nameVC.delegate = self when configuring the NameController in didSelectRowAtIndexPath since we implement NameControllerDelegate in ViewController. It is a common mistake to forget to set the delegate.

  • We deselect the row in didSelectRowAtIndexPath since we won't want it to be selected once we go back from the NameController.

  • We follow delegate method naming conventions by passing the delegating object (NameController) back as the first argument in our didSaveName method.

  • Without an unwind segue we have to manually pop top the view controller in didSaveName. When this method is called, this will be the NameController. Notice that we didn't have to set up this action for the Back button due to a built-in behavior.

Customizing the appearance of navigation bar

There are several ways to customize the appearance of the navigation bar and control what items appear in it.

Understanding navigation bars and navigation items

In order to know which object's properties we need to change to get the desired appearance, we first need to understand a bit about how navigation bars and navigation items work.

Each UINavigationController has a navigationBar of type UINavigationBar. Each UIViewController has an associated navigationItem of type UINavigationItem.

The navigation bar associated with a navigation controller maintains a stack of UINavigationItems that is parallel to the navigation stack of view controllers. When a view controller is pushed onto the navigation stack its navigationItem gets pushed onto the navigation bar's stack of navigation items.

The top and second from top UINavigationItems in the navigation bar's item stack are used to determine which components are added to navigation bar. Specifically the top navigation item determines the title view and button(s) on the right hand side of the nav bar. It can also determine the button(s) on the left hand side of the nav bar, but if no left-side button is set, the navigation bar will use the second from top navigation item in order to determine what "Back" button to show.

In some rarer cases you may want to use a UINavigationBar without the backing of a UINavigationController. In these instances, you may have to manually manage the UINavigationBar's navigation item stack by pushing/popping navigation items in order to achieve the appearance you want.

Customization inside a storyboard

When working in a project with storyboards, many of the things you might want to change about a navigation bar's appearance can be edited in the Attributes Inspector by selecting the navigation bar associated with your navigation controller.

The navigation item with your view controller can also be edited directly in the storyboard. For example, to add more buttons to the navigation item, you can simply drag buttons from the Object Library as we did in our storyboard example above. Note that a view controller's the navigation item will not be an element in your storyboard by default. You must drag a navigation item from the Object Library to your view controller (as we did in our storyboard example above) in order to have access to it—although, embedding your view controller in a navigation controller will automatically add the navigation item.

Navigation bar colors, background image, and text attributes

There are a few properties that you can manipulate in order to control the appearance of a navigation bar. Since a navigation controller is associated with a single navigation bar, any change here will be visible across all view controllers in a navigation controller's stack. Apple has been changing the behavior of these properties somewhat frequently in recent iOS versions, so you may have to experiment a bit to achieve the effect you want.

Continuing with our example from above we can customize our navigation bar as follows.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NameControllerDelegate {
    ...

    override func viewDidLoad() {
        ...

        self.navigationItem.title = "Names"
        if let navigationBar = navigationController?.navigationBar {
            navigationBar.setBackgroundImage(UIImage(named: "codepath-logo"), forBarMetrics: .default)
            navigationBar.tintColor = UIColor(red: 1.0, green: 0.25, blue: 0.25, alpha: 0.8)

            let shadow = NSShadow()
            shadow.shadowColor = UIColor.gray.colorWithAlphaComponent(0.5)
            shadow.shadowOffset = CGSizeMake(width: 2, high: 2)
            shadow.shadowBlurRadius = 4
            navigationBar.titleTextAttributes = [
                NSFontAttributeName : UIFont.boldSystemFont(ofSize: 22),
                NSForegroundColorAttributeName : UIColor(red: 0.5, green: 0.15, blue: 0.15, alpha: 0.8),
                NSShadowAttributeName : shadow
            ]
        }
    }
@implementation ViewController

- (void)viewDidLoad {
    ...

    self.navigationItem.title = @"Names";
    UINavigationBar *navigationBar = self.navigationController.navigationBar;
    [navigationBar setBackgroundImage:[UIImage imageNamed:@"codepath-logo"] forBarMetrics:UIBarMetricsDefault];
    navigationBar.tintColor = [UIColor colorWithRed:1.0 green:0.25 blue:0.25 alpha:0.8];
    
    NSShadow *shadow = [NSShadow new];
    shadow.shadowColor = [[UIColor grayColor] colorWithAlphaComponent:0.5];
    shadow.shadowOffset = CGSizeMake(2, 2);
    shadow.shadowBlurRadius = 4;
    navigationBar.titleTextAttributes = @{NSFontAttributeName : [UIFont boldSystemFontOfSize:22],
                                          NSForegroundColorAttributeName : [UIColor colorWithRed:0.5 green:0.15 blue:0.15 alpha:0.8],
                                          NSShadowAttributeName : shadow};

}

Title text and view of a navigation item

Notice that above we were able to set the navigation bar's title text by setting navigationItem.title on our view controller's navigation item. Since the navigation item is distinct per view controller, we can set separate titles for each view controller.

In rare cases you may wish to replace a navigation item's entire titleView. This is useful for example if you need customized styling of the title text only for a specific view controller. Continuing with our previous example

class NameController: UIViewController {
    ...

    override func viewDidLoad() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(NameController.saveButtonTapped)

        let titleLabel = UILabel()

        let shadow = NSShadow()
        shadow.shadowColor = UIColor.redColor().colorWithAlphaComponent(0.5)
        shadow.shadowOffset = CGSizeMake(width: 2, high: 2)
        shadow.shadowBlurRadius = 4

        let titleText = NSAttributedString(string: "Edit Name", attributes: [
            NSFontAttributeName : UIFont.boldSystemFont(ofSize: 28),
            NSForegroundColorAttributeName : UIColor(red: 0.5, green: 0.25, blue: 0.15, alpha: 0.8),
            NSShadowAttributeName : shadow
            ])

        titleLabel.attributedText = titleText
        titleLabel.sizeToFit()
        navigationItem.titleView = titleLabel

        ...
    }

}
@implementation NameController

- (void)viewDidLoad {
    ...

    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save"
                                                                              style:UIBarButtonItemStylePlain
                                                                             target:self action:@selector(saveButtonTapped:)];
    UILabel *titleLabel = [UILabel new];
    
    NSShadow *shadow = [NSShadow new];
    shadow.shadowColor = [[UIColor grayColor] colorWithAlphaComponent:0.5];
    shadow.shadowOffset = CGSizeMake(2, 2);
    shadow.shadowBlurRadius = 4;
    
    NSAttributedString *titleText = [[NSAttributedString alloc] initWithString:@"Edit Name"
                                                                    attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:28],
                                                                                 NSForegroundColorAttributeName : [UIColor colorWithRed:0.5 green:0.25 blue:0.15 alpha:0.8],
                                                                                 NSShadowAttributeName : shadow}];
    
    titleLabel.attributedText = titleText;
    [titleLabel sizeToFit];
    self.navigationItem.titleView = titleLabel;

}

Setting bar button items for a navigation item

You can also configure the button(s) that appear to the left and right of the title in a navigation item by providing your own UIBarButtonItem or array of UIBarButtonItems. The UIBarButtonItem also has a number of properties you can use to customize it. Of particular note is the fact that you can provide an arbitrary UIView as a custom view.

class NameController: UIViewController {
    ...

    override func viewDidLoad() {
        let saveButton = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(NameController.saveButtonTapped)

        let segmentedControl = UISegmentedControl(items: ["Foo", "Bar"])
        segmentedControl.sizeToFit()
        let segmentedButton = UIBarButtonItem(customView: segmentedControl)

        let dummyButton = UIBarButtonItem(title: "Dummy", style: .plain, target: nil, action: nil)

        navigationItem.rightBarButtonItems = [saveButton, segmentedButton]
        navigationItem.leftBarButtonItem = dummyButton
        ...
   }
   ...
}
@implementation NameController

- (void)viewDidLoad {
    ...
    UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithTitle:@"Save"
                                                                              style:UIBarButtonItemStylePlain
                                                                             target:self action:@selector(saveButtonTapped:)];
    
    UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems:@[@"Foo", @"Bar"]];
    [segmentedControl sizeToFit];
    UIBarButtonItem *segmentedButton = [[UIBarButtonItem alloc] initWithCustomView:segmentedControl];
    
    UIBarButtonItem *dummyButton = [[UIBarButtonItem alloc] initWithTitle:@"Dummy" style:UIBarButtonItemStylePlain target:nil action:nil];
    
    self.navigationItem.rightBarButtonItems = @[saveButton, segmentedButton];
    self.navigationItem.leftBarButtonItem = dummyButton;
}

The Back button

Notice that above when we set the top navigation item's leftBarButtonItem, the "Back" button no longer appeared. In order to customize the back button we have the leftBarButtonItem and leftBarButtonItems properties set to nil for top navigation item. We must manipulate the navigation item that is second from the top in the stack. This will generally be the navigation item associated with the view controller that just pushed the current view controller on top of the stack.

By default the navigation bar will use the second from top navigation item's title text as the back button text for the top view controller. We can customize this, for example, by having the back button say "Cancel" instead.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NameControllerDelegate {
     ...
     override func viewDidLoad() {
        ...

        self.navigationItem.title = "Names"
        self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: nil, action: nil)
        ...
    }

    func tableView(tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let nameVC = NameController()
        nameVC.fullName = names[indexPath.row]
        nameVC.delegate = self

        selectedIndexPath = indexPath
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
        self.navigationController?.pushViewController(nameVC, animated: true)

    }
    ...
}
@implementation ViewController

- (void)viewDidLoad {
    ...
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStylePlain target:nil action:nil];
}
Fork me on GitHub