Presenting Modal <span xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" class="cf class">UIViewController</span>ss – RubyMotion

Presenting Modal UIViewControllers

Sometimes we want one controller to take up the entire screen to get a user’s attention. For example, Mailapp’s New Message screen appears on top of the usual inbox list, forcing the user to either complete the message or explicitly end the task. To accomplish this, we present the controller modally.

UIViewControllers allows us to present modal view controllers at any point in their life cycle. The key method is presentViewController:animated:completion:, which functions similarly to UINavigationController’s pushViewController:animated:. The given controller will be presented above all other controllers in the app and will remain there until we invoke dismissViewControllerAnimated:completion:.

Let’s present a modal controller from our Top Color controller. The presented controller will allow us to change the top color, which is definitely a task best done while the rest of the interface is obscured.

First we need a button in the navigation bar at the top of the screen that presents this modal controller. We saw UIButton in Chapter 2, Filling the Screen with Views, but we need to use a different class for bar buttons: UIBarButtonItem. This isn’t a subclass of UIView; instead, it’s a plain-old Ruby object that we use to specify the text, image, and style of the bar button. The system will then take care of how to draw and add the button specifications as a view, much like UITabBarItem.

Let’s add our bar button to our app. In ColorDetailController, we create the button item in viewDidLoad like so:

  rightButton =
  UIBarButtonItem.alloc.initWithTitle(​"Change"​,
 style: ​UIBarButtonItemStyleBordered,
  target​:self​,
  action​:'change_color'​)
  self.navigationItem.rightBarButtonItem = rightButton
 end

We create our UIBarButtonItem instance with a title and a style. The style property determines how our button looks: it can be plain, bordered, or “done” (play around to see the difference). We then set the new UIBarButtonItem as our controller’s navigationItem’s rightBarButtonItem. Every UIViewController has a navigationItem, which is how we access all the information displayed in the top bar. Again, note that UINavigationItem is not a UIView, so you cannot add new subviews to it.

We also assign a target and action in the initializer, which function in the same manner as when we call addTarget:action:forControlEvents: on a UIButton.

We haven’t implemented change_color yet, so let’s get to it. To make modal controllers stand out even more, it’s a common practice to wrap them in a small UINavigationController. All we need to do is call presentViewController:animated:completion: with that controller, so it’s short and sweet.

 def​ change_color
  controller = ChangeColorController.alloc.initWithNibName(​nil​, bundle​:nil​)
  controller.color_detail_controller = self
  self.presentViewController(
  UINavigationController.alloc.initWithRootViewController(controller),
  animated​:true​,
 completion: ​lambda {})
 end

Everything looks normal except the lambda in completion. Just like the view animations we saw in Animating Views, presenting controllers take an anonymous callback function. We don’t need to do any special behavior right now, but it’s there if you ever need it. Our presented controller is a new ChangeColorController object, which is a class we don’t have yet.

Create change_color_controllerrb in ./app/controllers; this will be the controller that we actually present. It won’t be a super-complicated class: we’ll add a text field for the user to enter the color, as well as a button to enact that change. But before all that, we need to set up the plumbing.

 class​ ChangeColorController < UIViewController
 attr_accessor​ ​:color_detail_controller

We start with Ruby’s nifty attr_accessor to create the methods color_detail_controller and color_detail_controller=. We need these so we can easily store a reference to the ColorDetailController whose color we’re changing.

The ChangeColorController modal view also needs a UITextField and UIButton, both of which we covered in Chapter 2, Filling the Screen with Views. When the button is tapped, we’ll take whatever is in the text field and use that to create a UIColor that ColorDetailController can use. Just like the other controllers, this logic belongs in viewDidLoad.

 def​ viewDidLoad
 super
  self.title = ​"Change Color"
  self.view.backgroundColor = UIColor.whiteColor
  @text_field = UITextField.alloc.initWithFrame(CGRectZero)
  @text_field.borderStyle = UITextBorderStyleRoundedRect
  @text_field.textAlignment = UITextAlignmentCenter
  @text_field.placeholder = ​"Enter a color"
  @text_field.frame = [CGPointZero, [150,32]]
  @text_field.center =
  [self.view.frame.size.width / 2, self.view.frame.size.height / 2 - 170]
  self.view.addSubview(@text_field)
  @button = UIButton.buttonWithType(UIButtonTypeSystem)
  @button.setTitle(​"Change"​, forState​:UIControlStateNormal​)
  @button.frame = [[
  @text_field.frame.origin.x,
  @text_field.frame.origin.y + @text_field.frame.size.height + 10
  ],
  @text_field.frame.size]
  self.view.addSubview(@button)
  @button.addTarget(self,
  action​:"change_color"​,
  forControlEvents​:UIControlEventTouchUpInside​)
 end

It’s lengthy, but most of the code just lays out our views. We position @text_field slightly above the center of the view and then position @button right below. Finally, we set our callback to be change_color, so we need to write that too.

 def​ change_color
  color_text = @text_field.text
  color_text ||= ​""
  color_text = color_text.downcase
  color_method = ​"​​#{​color_text​}​​Color"
 if​ UIColor.respond_to?(color_method)
  color = UIColor.send(color_method)
  self.color_detail_controller.view.backgroundColor = color
  self.dismissViewControllerAnimated(​true​, ​completion: ​​nil​)
 return
 end
 
  @text_field.text = ​"Error!"
 end
 end

First we try to generate a UIColor from @text_field.text. We use respond_to? to catch invalid colors (like “catdogColor”), but if nothing bad happens, then we forge ahead. We grab a reference to our ColorDetailController using color_detail_controller, set its background color, and then dismiss ourselves with dismissViewControllerAnimated:completion:. Not bad at all, right?

Take our app for a spin, and everything should go smoothly, as in the following figure:


Figure 6. Changing the color from a text field

Make sure to test our exception handling in ChangeColorController, as well some less-known UIColor helpers like magenta or cyan.

We made some really tangible progress in this chapter as we built software that looks and acts like what’s expected on iOS. The code we went through should give you a better idea about the biggest difference between the iOS APIs and plain Ruby: the original Objective-C method names are very verbose. Need I say even more than UIViewAutoresizingFlexibleBottomMargin? Thankfully, we pulled a few Ruby tricks with UIColor.send and saved even more boilerplate by removing the need for header files and complex class definitions just with attr_accessor.

Our apps can now be organized using the standard UI patterns, but that last example showed a significant gap in our knowledge: changing data can be messy. Passing the ColorDetailController as a property and altering its view directly is less than desirable. We’re going to cover a more automatic way of handling those kind of data changes and more in Chapter 4, Representing Data with Models.

Footnotes

[13]

RubyMotion does support Interface Builder; simply add your NIB or XIB files to the project’s ./resources directory. Using Interface Builder is beyond the scope of this text, but you can use the IB RubyGem (https://github.com/RubyMotion/ib) to help connect your RubyMotion code inside Interface Builder.

[14]

http://developer.apple.com/library/ios/#documentation/uikit/reference/UIViewController_Class/Reference/Reference.html