The examples so far have been single view apps, except the view flipping example. In a real app, though, you have lots of views, and we should do some practice with that. I think it’s time we brought in the UINavigationController.

In app_delegate.rb we need to instiate both the navigation controllers view, and the root view that it should display first.

Our root view will be (surprise!) a table view.

“`ruby class AppDelegate def application(application, didFinishLaunchingWithOptions:launchOptions) @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)

mine = RootViewController.alloc.init
@window.rootViewController = UINavigationController.alloc.initWithRootViewController(mine)

@window.makeKeyAndVisible

true

end end ”`

Most of this code comes from my original UITableView example, with just a few changes:

  1. Assign self as the delegate
  2. push a “detail view” onto the navigation controller stack when an item is selected

“`ruby class RootViewController < UIViewController def viewDidLoad self.title = "The playas”

@data = [
  {first: 'firsty', last: 'McLasty'},
  {first: 'humphrey', last: 'bogart'},
]

@table = UITableView.alloc.initWithFrame([[0, 0], [self.view.frame.size.width, self.view.frame.size.height]],
                                   style: UITableViewStylePlain)
@table.dataSource = self
@table.delegate = self
view.addSubview(@table)

end

# new code! def tableView(tableView, didSelectRowAtIndexPath:indexPath) # we could cache these in a hash or something, but that’s not necessary # right now, I think… view_controller = DetailViewController.alloc.initWithCharacter(@data[indexPath.row])

tableView.deselectRowAtIndexPath(indexPath, animated:true)
self.parentViewController.pushViewController(view_controller, animated: true)

end

############################### IDENTICAL CODE ############################### def tableView(tableView, cellForRowAtIndexPath:indexPath) cell = tableView.dequeueReusableCellWithIdentifier(cellidentifier) || UITableViewCell.alloc.initWithStyle( UITableViewCellStyleDefault, reuseIdentifier: cellidentifier) cell.textLabel.text = “#{@data[indexPath.row][:first]} #{@data[indexPath.row][:last]}”

return cell

end

def cellidentifier @@cellidentifier ||= ‘Cell’ end

def tableView(tableView, numberOfRowsInSection: section) case section when 0 @data.length else 0 end end ############################### ////////////// ###############################

end “`

The code that deselects the cell is necessary if you implement the didSelectRowAtIndexPath: delegate method.

So, we instantiate our next view controller and push it onto the stack. We can get our "parent” controller using self.parentViewController. Pretty easy, I would say!

The detail views are handed a “character” to display:

“`ruby

displays the first and last name

class DetailViewController < UIViewController

def initWithCharacter(character) @character = character init end

def viewDidLoad self.title = ”#{@character[:first]} #{@character[:last]}“

@first = UITextField.alloc.initWithFrame([[10, 30], [300, 40]])
@first.borderStyle = UITextBorderStyleRoundedRect
@first.text = @character[:first]
self.view.addSubview(@first)

@last = UITextField.alloc.initWithFrame([[10, 80], [300, 40]])
@last.borderStyle = UITextBorderStyleRoundedRect
@last.text = @character[:last]
self.view.addSubview(@last)

end

end ”`

We can run our program at this point!

  • screenshot_navigation_root.png

    Screenshot Navigation Root Png

  • screenshot_navigation_transition_ugly.png

    Screenshot Navigation Transition Ugly Png

  • screenshot_navigation_detail_ugly.png

    Screenshot Navigation Detail Ugly Png

Background look right to you? Heck no. Easy fix (with a nod to those who have blazed this trail before me):

ruby self.view.backgroundColor = UIColor.groupTableViewBackgroundColor

But those text fields don’t look right, other than the border being “rounded” (the default is no border at all - it looks pretty bad).

Apple made the ahem interesting choice that views made in XCode would not look the same as the same views made programmatically. Hmph. Those should look like this:

  • uitextfield_corrected.png

    Uitextfield Corrected Png

Here’s the pile ‘o’ code we need to add to match the XCode defaults:

(note: the contentVerticalAlignment property isn’t even documented anywhere! I don’t know how other people even discovered this property)

ruby @first.font = UIFont.systemFontOfSize(14) @first.minimumFontSize = 17 @first.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter @first.adjustsFontSizeToFitWidth = true @first.placeholder = "First Name"

Adding this to both UITextFields, we now get the beautiful:

  • screenshot_navigation_root.png

    Screenshot Navigation Root Png

  • screenshot_navigation_transition.png

    Screenshot Navigation Transition Png

  • screenshot_navigation_detail.png

    Screenshot Navigation Detail Png

Next last trick: save the changes! Easy to update the @character Hash that we’ve passed the DetailViewController, so why not?

When we press “Return”, we will transfer focus to the next input. That’s an easy method, we’ll send becomeFirstResponder to the object we want to have the focus.

ruby def textFieldShouldReturn(text_field) if text_field == @first @last.becomeFirstResponder else @first.becomeFirstResponder end end

Run it, neat, you can press Return. When the text field loses it’s first responder status - either by pressing return or pressing the nav-controller “back” button, we will update the @character property. We are gonna be elegant about this, and only update the property that was edited. We could just save the entire object when they press back.

ruby def textFieldDidEndEditing(text_field) if text_field == @first @character[:first] = text_field.text else @character[:last] = text_field.text end end

Another way to save

I mentioned that we could save the entire object, and that’s worth looking at, too, since this is supposed to be an example. It’s easy, and this way might be better for your application.

ruby def textFieldDidEndEditing(text_field) @character[:first] = @first.text @character[:last] = @last.text end

You can now edit the character, and if you go back and forth you’ll see your changes are saved… except… hey, they are NOT being refreshed in the table! Bullocks! We need to tell the table to update (or refresh.. redraw? reload!).

We call the reloadData method on our tableView. We can do this one of two ways:

  1. Call if from within RootViewController, just before it becomes the active viewController
  2. Call from the DetailViewController after every change.

Method 1

We make the RootViewController the delegate to the UINavigationController, and implement the willShowViewController delegate method:

“`ruby def viewDidLoad self.title = "The playas” self.parentViewController.delegate = self #… as before end

and add this method:

def navigationController(navigationController, willShowViewController:viewController, animated:animated) if viewController == self tableView.reloadData end end “`

SWEEET. But! This method always refreshes, no matter what view we come from. Fine for now, perhaps, but it IS unnecessary processor ticks.

Method 2

Instead, we can call reloadData from our detail view, but we’ll need a way to access the table.

Back in RootViewController, give read access to the @table property:

ruby class RootViewController < UIViewController attr_reader :table #...

In DetailViewController, we can indirectly access the RootViewController using the parentViewController property to get the UINavigationController, and then getting the first ([0]) view controller from the viewControllers list.

I tried, first, to use the topViewController, but it returned the DetailViewController. I don’t know why there is both topViewController AND visibleViewController, but whatever, I guess Apple needed this distinction…

Anyway, it doesn’t matter because this line of thought won’t work! When you press the "Back” button to go back to the “playas” table, the order of events goes something like this:

  1. the back button calls popViewController on the UINavigationController
  2. the parentViewController property is set to nil
  3. the view is unloaded:
    1. the text field that has focus is unfocused, by calling textFieldShouldEndEditing and then textFieldDidEndEditing. By this time self.parentViewController is already nil.
  4. the other view is loaded - maybe this happens before #3, it doesn’t matter.
  5. the cool scrolling effect happens.

Instead, we will need to give the view controller access to the RootViewController.

DetailViewController

“`ruby class DetailViewController < UIViewController attraccessor :playascontroller # … def textFieldDidEndEditing(textfield) if textfield == @first @character[:first] = textfield.text else @character[:last] = textfield.text end

@playas_controller.table.reloadData

end # … ”`

RootViewController

ruby def tableView(tableView, didSelectRowAtIndexPath:indexPath) view_controller = DetailViewController.alloc.initWithCharacter(@data[indexPath.row]) view_controller.playas_controller = self # ...

It would be even better to provide a method in RootViewController that the DetailViewController can call to refresh the data, instead of giving it access to the table view. That would mean that if you swapped out the table view for something else, you could update that method, and not the DetailViewController. But that’s overkill for this example (it’s pretty much overkill in general).