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.

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
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(cell_identifier) ||
          UITableViewCell.alloc.initWithStyle( UITableViewCellStyleDefault,
                              reuseIdentifier: cell_identifier)
    cell.textLabel.text = "#{@data[indexPath.row][:first]} #{@data[indexPath.row][:last]}"

    return cell
  end

  def cell_identifier
    @@cell_identifier ||= '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:

# 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):

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)

@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.

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.

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.

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:

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:

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

class DetailViewController < UIViewController
  attr_accessor :playas_controller
  # ...
  def textFieldDidEndEditing(text_field)
    if text_field == @first
      @character[:first] = text_field.text
    else
      @character[:last] = text_field.text
    end

    @playas_controller.table.reloadData
  end
  # ...

RootViewController

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).