Using Multiple Controllers with UINavigationController – RubyMotion

Using Multiple Controllers with UINavigationController

Although some iOS apps are famous for their unique visuals, most apps share a common set of interface elements and interactions included with the SDK. Typical apps will have a persistent top bar (usually blue) with a title and some buttons; these apps use an instance of UINavigationController, one of the standard container controllers in iOS.

Containers are UIViewController subclasses that manage many other child UIViewControllers. Kind of wild, right? Containers have a view just like normal controllers, to which their children controllers’ views are added as subviews. Containers add their own UI around their children and resize their subviews accordingly. UINavigationController adds a navigation bar and fits the children controllers below, like this:

UINavigationController manages its children in a stack, pushing and popping views on and off the screen. Visually, new views are pushed in from the right, while old views are popped to the left. For example, Mailapp uses this to dig down from an inbox to an individual message. UINavigationController also automatically handles adding the back button and title for you; all you need to worry about is pushing and popping the controller objects you’re using. Check out the following to see how Settingsapp uses navigation controllers.

UINavigationController is pretty easy to integrate. In AppDelegate, we change our rootViewController assignment to use a new UINavigationController.

 class​ AppDelegate
 def​ application(application, didFinishLaunchingWithOptions​:launchOptions​)
  @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
  @window.makeKeyAndVisible
» controller = ColorsController.alloc.initWithNibName(​nil​, ​bundle: ​​nil​)
» nav_controller =
» UINavigationController.alloc.initWithRootViewController(controller)
» @window.rootViewController = nav_controller
 true
 end
 end

initWithRootViewController: will take the given controller and start the navigation stack with it. As we said earlier, the UINavigationController will handle adding and resizing this controller’s view to fit to the appropriate size.

Before we run the app, we should make one more change in ColorsController. Every UIViewController has a title, which UINavigationController uses to set the top bar’s title.

 self.view.addSubview(@label)
»self.title = ​"Colors"

Run and check out our slightly prettier app:

Excellent, let’s make it do something. We’ll add a few buttons to our view, each representing a color. When a button is tapped, we’ll push a detail controller with its color as the background. Visually, a new controller will slide in from the right while the old ColorsController fades to the left.

First we need to add those buttons to the view at the end of viewDidLoad. We’re going to use some of Ruby’s dynamic features to get this done, primarily the send method. You can use any of the default UIColor helper methods like purpleColor or yellowColor, but we’re going to stick with the basics.

 [​"red"​, ​"green"​, ​"blue"​].each_with_index ​do​ |color_text, index|
  color = UIColor.send(​"​​#{​color_text​}​​Color"​)
  button_width = 80
 
  button = UIButton.buttonWithType(UIButtonTypeSystem)
  button.setTitle(color_text, forState​:UIControlStateNormal​)
  button.setTitleColor(color, forState​:UIControlStateNormal​)
  button.sizeToFit
  button.frame = [
  [30 + index*(button_width + 10),
  @label.frame.origin.y + button.frame.size.height + 30],
  [80, button.frame.size.height]
  ]
  button.autoresizingMask =
  UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin
  button.addTarget(self,
  action​:"tap_​​#{​color_text​}​​"​,
  forControlEvents​:UIControlEventTouchUpInside​)
  self.view.addSubview(button)
 end

See the color = UIColor.send("#{color_text}Color") trick? This lets us create the UIColor, button text, and button callback all with a single color_text variable. If you run our app now, you should see the three buttons just like this (but don’t tap any quite yet):

Now on to implementing those button callbacks. For each of these, we’ll need to push a new view controller onto our UINavigationController’s stack. UIViewControllers happens to have a navigationController property, which lets us access the parent UINavigationController. This navigationController is automatically set whenever we add a view controller to a navigation stack, which we did with initWithRootViewController:. With that in mind, our button callbacks look something like this:

 def​ tap_red
  controller = ColorDetailController.alloc.initWithColor(UIColor.redColor)
  self.navigationController.pushViewController(controller, ​animated: ​​true​)
 end
 def​ tap_green
  controller = ColorDetailController.alloc.initWithColor(UIColor.greenColor)
  self.navigationController.pushViewController(controller, ​animated: ​​true​)
 end
 def​ tap_blue
  controller = ColorDetailController.alloc.initWithColor(UIColor.blueColor)
  self.navigationController.pushViewController(controller, ​animated: ​​true​)
 end

We call pushViewController:animated: on the navigation controller, which pushes the passed controller onto the stack. By default, the navigation controller will also create a back button that will handle popping the frontmost child controller for us. If you need to do that programmatically, just call popViewControllerAnimated: on UINavigationController.

We referenced a new ColorDetailController class, so let’s implement that. First we create color_detail_controllerrb in ./app/controllers. We’ll keep it simple and just set the title and background color.

 class​ ColorDetailController < UIViewController
 def​ initWithColor(color)
  self.initWithNibName(​nil​, bundle​:nil​)
  @color = color
  self.title = ​"Detail"
  self
 end
 def​ viewDidLoad
 super
 
  self.view.backgroundColor = @color
 end
 end

Start by defining a new initializer, initWithColor:, which takes a UIColor as its argument. Our implementation of this method uses UIViewController’s designated initializer initWithNibName:bundle:, which is required for any controller initializer we write. You also need to return self from these functions, which should make sense given all the times we assign a variable from these methods (such as controller = UIViewController.alloc.initWithNibName(nil, bundle: nil)).

It’s time to rake and play with our navigation stack. It should look like this:

Check out the slick animations on the navigation bar, where the title simultaneously fades and slides as a new controller is pushed.

Many apps structure their interface using UINavigationController, where each pushed controller gradually reveals more detailed data. As you saw, there are only a couple of methods we need to implement that user interface. However, some apps need more than this kind of hierarchal layout. UITabBarController is another widely used container controller, and in Separating Controllers with UITabBarController we’re going to add it to our app.