Navigation Controller

Click anywhere on a big yellow view (except where it’s covered by the translucent navigation bar) to go east to the next station of the L subway.

The status bar is the top 20 pairs of pixels of the iPhone 6 screen. It displays the current time, battery level, etc. Under the status bar is the navigation bar. See Bars.

In portrait orientation, the heights of the status and navigation bars are 20 and 44 pairs of pixels, for a total of 64. We therefore set the yellow view’s bounds.origin to (–64, 0), which puts the origin (0, 0) at the lower left corner of the navigation bar. In landscape orientation, the height of the navigation bar is 32 pairs of pixels. bounds.origin is therefore set to (–32, 0), which puts the origin (0, 0) at the lower left corner of the navigation bar.

The status and navigation bars, not to mention the back button, looked better in iOS 6. What were they thinking of?

The tab bar controller we saw here let us visit the subordinate view controllers in any order. A navigation controller lets us visit the subordinate view controllers in only one specific order: Eighth Avenue, Sixth Avenue, Union Square, etc., and back again. You can’t jump directly from Eighth Avenue to Union Square without visiting Sixth Avenue along the way.

The classic example of a navigation controller is in the Settings app that comes with the iPhone. The navigation controller provides the right-to-left animation as we drill deeper into the menus, and left-to-right animation as we come back out. It does not provide the menus themselves—they will come later, when we have class UITableView.

Even if we have only one view controller and one view, we might still want to put a navigation controller atop the view controller just to make the view controller’s navigation bar visible. Every view controller has a navigation bar, but the navigation bar is visible only if there is a navigation controller above the view controller. The navigation bar can contain attractive titles and buttons.

The Main.storyboard file creates the LineController object, whose class is a subclass of class UINavigationController. The LineController contains an array of one or more StationControllers, one for each station on the L Train, Manhattan’s only east/west subway line. Each StationControllers, creates one StationView.

The init method of class LineController creates only the first StationController, not all of them, because it might be expensive to create objects of this class. We create the StationControllers only as needed.

Source code in Navigate.zip

  1. Class AppDelegate: unchanged.
  2. Class LineController is a subclass of class UINavigationController, which is a subclass of class UIViewController. This app has only one object of class LineController.
  3. Class StationController is a subclass of class UIViewController. Underneath the LineController, this app has an array containing (eventually up to) six objects of class StationController.
  4. Class StationView. Each StationController creates one object of this class. We had to set the view’s contentMode in Tabbar. The \n in drawRect(_:) is the newline character.

Create the project

Class StationView is a subclass of class UIView, class StationController is a subclass of class UIViewController, and class LineController is a subclass of class UINavigationController.

After creating these three classes, go to the Xcode Project Navigator and select the file Main.storyboard. Open the left pane of the center panel of Xcode as far as

▼ View Controller Scene
   ▶ View Controller
   First Responder
   Exit
and select the View Controller. In the right panel of Xcode, click on the icon for the Identity inspector. It’s a rectangle with a smaller rectangle in its upper left corner.
Custom Class
Class: LineController
Module: (just leave it blank)

In the Xcode Project Navigator, control-click on the file ViewController.swift and select Delete.
Do you want to move the file "ViewController.swift" to the Trash?
Move to Trash.

Things to try

  1. To see the animation more clearly, give each view a different background color. In the init method of class StationView, change the background color to the following. We saw the function arc4random in Puzzle. With no parameter, it returns a random UInt32 in the range 0 to UInt32.max = 4,294,967,295 inclusive. Each quotient is therefore in the range 0.0 to Without the conversions to a floating point type, the quotients would be 0. 1.0 inclusive.
    		backgroundColor = UIColor(
    			red:   CGFloat(arc4random()) / CGFloat(UInt32.max),
    			green: CGFloat(arc4random()) / CGFloat(UInt32.max),
    			blue: 1.0,
    			alpha: 1.0);
    

  2. In the next method of class LineController, what happens if you change the animated: to false? After you find out, change it back to true.

  3. The navigation bar displays the title of the view controller that is currently displayed by the navigation controller. To add a prompt to the top of the navigation bar (occcupying an additional 30 pixels), insert the following statement into the init method of class StationController after setting the title.
    		navigationItem.prompt = "Welcome to the \(title) station!";
    
    Observe that the text in the StationView has moved down to accomodate the greater height of the navigation bar. On iPhone 6, it’s 74 pairs of pixels in portrait, 54 in landscape. I acknowledge that the prompt is inane. After you’ve seen it, get rid of it.

  4. The navigation bar already has a button in the upper left corner (except when we’re at the first station, 8th Avenue). Let’s add a button in the upper right corner. Insert the following statement into the viewDidLoad method of class StationController. (I would have preferred to insert it into the init method of class StationController, after setting the title and prompt. But at that early point, the navigationController property of the StationController is still nil).
    		navigationItem.rightBarButtonItem = UIBarButtonItem(
    			title: "Go East",
    			style: UIBarButtonItemStyle.Plain,
    			target: navigationController!,
    			action: "next");
    
    Even better, prevent the last station (Bedford Avenue) from having a “Go East” button.
    		let lineController: LineController = navigationController! as LineController;
    		if lineController.viewControllers.count < lineController.titles.count {
    			navigationItem.rightBarButtonItem = UIBarButtonItem(
    				title: "Go East",
    				style: UIBarButtonItemStyle.Plain,
    				target: navigationController!,
    				action: "next");
    		}
    
    What would happen if you changed the above "Go East" to lineController.titles[lineController.viewControllers.count]? You can now remove the touchesEnded(_:withEvent:) from class StationView. Also take a look at the various types of UIBarButtonSystemItems.


  5. In the int method of the LineController, after the call to super.init, set the toolbarhidden property of the LineController to false.
    		toolbarHidden = false;
    
    The tool bar that appears at the bottom of the screen will be empty. We will use a tool bar here.

  6. Instead of putting the array of title data into the LineController, should we have put it into a separate object called the model? Then we would have all three components: model/view/controller.

  7. We currently deinitialize (destroy) a StationController (and its StationView) each time we retreat one station to the west, and re-create the StationController (and its StationView) each time we advance one station to the east. We can verify this by adding the following method to class StationController. (Actually, this method doesn’t get called. I wonder why not.)
    	deinit {
    		print("StationController deinit \(title)");
    	}
    
    It would save time if we could create the StationController the first time we visit it, and keep it stored in an array in case we visit it again in the future. Give the LineController the following property.
    	//all the stations we have already visited at any time
    	var visited: [StationController] = [StationController]();	//an empty array of StationControllers
    
    In the init method of the LineController, before the pushViewController(_:animated:), insert the following statement.
    		visited.append(firstStationController);
    
    Change the next method of the LineController to the following.
    	func next() {
    
    		let i: Int = viewControllers.count;
    		if i == titles.count {
    			//We are currently visiting the last station, and can go no farther.
    			return;
    		}
    
    		if visited.count <= i {
    			//This station is being visited for the first time.
    			//Create a StationController for it.
    			let nextStationController: StationController = StationController(title: titles[i]);
    			visited.append(nextStationController);
    		}
    
    		pushViewController(visited[i], animated: true);
    	}