Review, rinse, repeat

We’re going to build up yet another UITableView, and we’ll be using the UINavigationController again while we do.

I would also like to revisit how the UITableView is reloaded. Previously I just manually tossed in a call to reloadData whenever the textfield was unfocused, but this is some pretty tight coupling.

Start with a Table, then make it navigable.

I’m going to first copy the code from my first UITableView tutorial, and then, just because it is sooo easy, and worth showing, I’ll “wrap” that in a UINavigationController. Meaning, the app will start out with only a single UIViewController, but then I will instantiate a UINavigationController and move the view controller inside of it.

The AppDelegate looks like this for now. I’m just instantiating a view controller and assigning it to the window:

“`ruby class AppDelegate

def application(application, didFinishLaunchingWithOptions:launchOptions) @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) @window.makeKeyAndVisible

@window.rootViewController = MyApplicationController.alloc.init



end ”`

I’m starting with the exact same UITableView cell code in MyApplicationController, so I’m not going to repeat it here (it is in the github repo, and you can read about it in the original post).

I immediately add the code to deselect the touched cell, from the custom table cell tutorial. It should really have been in the original UITableView post, but oh well, too late now.

ruby def tableView(tableView, didSelectRowAtIndexPath:indexPath) tableView.deselectRowAtIndexPath(indexPath, animated:true) end

With this in place, we are ready to turn this into a UINavigationController-powered application.

Ready? It’s gonna be a quick change, so watch for it!

“`ruby class AppDelegate

def application(application, didFinishLaunchingWithOptions:launchOptions) @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) @window.makeKeyAndVisible

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



end ”`

“`diff — appdelegate.rb +++ appdelegate.rb (Unsaved) @@ -4,7 +4,8 @@ @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) @window.makeKeyAndVisible

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

    true end ”`

And in the MyApplicationController class, be a gentleman and give the table a name:

“`ruby class MyApplicationController < UIViewController

def viewDidLoad self.title = "Editable Tableview” #… “`

Pretty cool, huh? total of three lines, we’re out of the gates.

  • tableview_in_a_nav.png

    Tableview In a Nav Png

Editing rows

Let’s add a view to edit a row. This will be used later to add a new row, so we’ll have a little code to help with that.

When the user presses a row (aka "in the tableView(tableView, didSelectRowAtIndexPath:indexPath) method”) we will lazily instantiate an EditPlayerController, assign the player to it, and then push it onto the stack. We will be reusing this view controller throughout the lifetime of the app, so we need to make sure we reset it whenever a new row is touched. Assigning nil will be a clue that we are adding, not editing, a player.

I’m grabbing code from a previous post where I played with a UINavigationController. Unlike in that post, I will not re-instantiate a new controller everytime they touch an item. Depending on how heavy that detail view is, I could see an argument for re-using one view or instantiating and destroying a fresh view every time (which also keeps the code simpler, I think).

I am also moving the UITextField initialization into custom methods. This is nice for two reasons: 1, that view code is better organized. 2, The player= method will be called before the viewDidLoad method is called, and it assigns the first and last names to the views. So using @first ||= ... makes this easy - whenever the view is needed, it will be there.

“`ruby class EditPlayerController < UIViewController

def viewDidLoad # labels and textfields for the first and last names go here self.view.backgroundColor = UIColor.groupTableViewBackgroundColor



def first_field @first ||= UITextField.alloc.initWithFrame([[10, 30], [300, 40]]).tap do |first| first.borderStyle = UITextBorderStyleRoundedRect first.font = UIFont.systemFontOfSize(14) first.minimumFontSize = 17 first.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter first.adjustsFontSizeToFitWidth = true first.placeholder = "First Name” first.delegate = self end end

def last_field @last ||= UITextField.alloc.initWithFrame([[10, 80], [300, 40]]).tap do |last| last.borderStyle = UITextBorderStyleRoundedRect last.font = UIFont.systemFontOfSize(14) last.minimumFontSize = 17 last.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter last.adjustsFontSizeToFitWidth = true last.placeholder = “Last Name” last.delegate = self end end

def player=(player) @player = player # we will be using this view to add a player, in which case player will be nil. if not player # assign empty or default values self.title = “” @first.text = “” @last.text = “” else # assign values from player self.title = “#{@player[‘first’]} #{@player[‘last’]}” @first.text = @player[‘first’] @last.text = @player[‘last’] end end

# pressing return moves the cursor to the next text field def textFieldShouldReturn(textfield) if textfield == @first @last.becomeFirstResponder else @first.becomeFirstResponder end end

# saves the changes def textFieldDidEndEditing(textfield) if @player if textfield == @first @player[‘first’] = textfield.text else @player[‘last’] = textfield.text end end end

end “`

I want to improve the way the tableview gets updated. Calling reloadData on the tableview manually (which is what I did in the previous post) seems very inelegant. I would prefer to use key-value observation to listen to changes to the player, and key-value binding to attach the text fields to the first and last name properties of the player.

But I could not easily do this. The observer methods never got fired, and the binding methods simply do not exist. Maybe this stuff is not ready in iOS? Not sure. Frustrating. The last recourse is not terrible, though, which is to use a notification center. When a player is changed or added, a "changed” notification is sent, and the tableview is updated accordingly. The upside is that I can re-use this method both during editing and when adding and removing players.


ruby def textFieldDidEndEditing(text_field) # ... NSNotificationCenter.defaultCenter.postNotificationName("players changed", object:self) end


“`ruby def viewDidLoad # … NSNotificationCenter.defaultCenter.addObserver(self, selector: :playersChanged, name:@"players changed”, object:nil) end

def playersChanged @table.reloadData end “`

At this point we have caught up with the previous post, and we are ready to…

Reorder and Delete Players

This had me scratching my head for a while, because there are a bunch of methods that need to be in place before it works.

First, we add an "edit” button to the navigation controller leftBarButtonItem property. Since we are adding this to the root view controller, there is no chance of a back button being there, otherwise you would have to add it to the rightBarButtonItem (alongside the “add” button, which we add below).

ruby def viewDidLoad # ... self.navigationItem.leftBarButtonItem = self.editButtonItem end

That’ll add an edit button, and by default (since we use the built-in editButtonItem) it will toggle between “Edit” and “Done” when you press it, but that’s all it does. We need to implement the setEditing(animated:) method, and in there, we tell the table view it is being edited. If you were not using a table view, but some custom view or views, you would set them up for “editing mode” in this method (the phone book app does this).

ruby def setEditing(is_editing, animated:is_animated) # tell the table view we are (or aren't) in editing mode @table_view.setEditing(is_editing, animated:is_animated) # pass to super, which toggles the "Edit" and "Done" button label super end

Now when you press Edit, you’ll get the familiar delete button, but no reorder button. That’s because the tableview has inspected its data source, and found that it lacks a tableView(moveRowAtIndexPath:toIndexPath:) method. Also, if you press delete, it just hangs there as if in mid touch. We have work to do!

  • tableview_delete_mode.png

    Tableview Delete Mode Png

Let’s add the data source method, and while we’re at it, we will implement the code that we need to actually reorder the rows in the @players array. If we don’t, the code will still “work”, until the table view gets a reloadData message, at which point the change (which was never actually saved) would be reverted. So heads up, you might hit your head against this for a while if you think it all “just works” the first time you give it a whirl.

ruby def tableView(tableView, moveRowAtIndexPath:from_index_path, toIndexPath:to_index_path) # why is this an attribute, not a local variable? good ol' rubymotion garbage # collection was breaking my balls here. Using an attribute fixed it, so # there it is. support ticket has been filed... @move = @players.delete_at(from_index_path.row) if @move @players.insert(to_index_path.row, @move) end end

And now we have drag handles! To test this, reorder the rows and then press one to edit it. You’ll find that the edit view and the row orders are in sync.

  • tableview_edit_mode.png

    Tableview Edit Mode Png

To delete a row, we’ll do pretty much the same thing. Interestingly it is not a special “delete” method that gets called on the data source, but a generic tableView(commitEditingStyle:forRowAtIndexPath:) method, that can get called when a row is inserted or deleted. I don’t understand the benefit / logic of this, especially since the insert action isn’t even available by default. We will implement an add button, but it will not use this feature.

ruby def tableView(tableView, commitEditingStyle:editing_style, forRowAtIndexPath:index_path) if editing_style == UITableViewCellEditingStyleDelete @players.delete_at(index_path.row) @table_view.deleteRowsAtIndexPaths([index_path], withRowAnimation:UITableViewRowAnimationAutomatic) end end

Now reorder and delete to your hearts content. Which is, hopefully, twice, since we only have two rows…

If only we could have a feature where we could somehow…

Add a player!

We’ll need an add button, which is pretty dang easy, and then we’ll hook it up to the EditPlayerController, passing nil to player= method. To make it oh-so-iOS-y, we will display it in a modal, with a cancel button in the upper-right corner.

ruby def viewDidLoad # ... self.navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem( UIBarButtonSystemItemAdd, target: self, action: :addPlayer)

The addPlayer method will use a new instance of EditPlayerController. I know I said we’d be using the same one, but that was a damn lie. The reason is that the customizations that we will need to make in order for the modal window to look right are so much, that doing and undoing them for the edit vs add modes would be tedious. Instead, we will customize the add_player_controller when we create it. The code looks much better, trust me.

“`ruby def addplayercontroller @addplayercontroller ||= do |ctlr| ctlr.navigationItem.leftBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem( UIBarButtonSystemItemCancel, target: self, action: :cancelAddPlayer)

  ctlr.navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem(
      target: self,
      action: :doneAddPlayer)

  ctlr.navigationItem.rightBarButtonItem.enabled = false

end end

def addPlayer # assign the player (resets the inputs) self.addplayercontroller.player = nil

# create and customize the navigation controller. This gives us an easy # place to put the "Cancel” and “Done” buttons, and a “New Player” title. ctlr = UINavigationController.alloc.initWithRootViewController(self.addplayercontroller) ctlr.modalTransitionStyle = UIModalTransitionStyleCoverVertical ctlr.delegate = self

self.presentViewController(ctlr, animated:true, completion:nil) end “`

cancelAddPlayer and doneAddPlayer will both dismiss the dialog, and that is all that cancelAddPlayer will do, so let’s get that out of the way:

ruby def cancelAddPlayer self.dismissViewControllerAnimated(true, completion:nil) end

In doneAddPlayer we also need to add the player, if it is returned successfully by the EditPlayerController#player method. If the entry is invalid, we return nil from that method. First, the doneAddPlayer method, then we’ll move back into EditPlayerController class.

”`ruby def doneAddPlayer if self.addplayercontroller.player @players.push(self.addplayercontroller.player) self.playersChanged end

self.dismissViewControllerAnimated(true, completion:nil) end “`

Back in EditPlayerController now, we will add some methods and conditions for adding a new player. First, let’s write that player method:

”`ruby def player firstname = self.firstfield.text.tos lastname = self.lastfield.text.tos

if firstname.length + lastname.length > 0 { ‘first’ => firstname, ‘last’ => lastname, } else nil end end “`

Next, I want to enable/disable the "Done” button, depending on whether the user has entered a name (either name will do).

This turned out to be kind of a pain in the butt. You can either use the delegate method textField(shouldChangeCharactersInRange:replacementString:), which gets fired everytime the text changes, or you can register (and later unregister) for UITextFieldTextDidChangeNotification events.

The delegate method is screwy, because the edit hasn’t actually happened yet, so you have to check the contents of the replacementString argument along with checking all the other inputs. Bleh. The notification way is only complicated by the requirement that we will need to register for the notification. Easy!

“`ruby def textFieldDidBeginEditing(text_field) NSNotificationCenter.defaultCenter.addObserver(self, selector: "textFieldChanged”, name: UITextFieldTextDidChangeNotification, object: nil) end

def textFieldDidEndEditing(text_field) NSNotificationCenter.defaultCenter.removeObserver(self) # … end “`

Now we can check our text fields, and enable/disable the rightBarButtonItem button accordingly:

ruby def textFieldChanged if not @player # enable or disable the button, depending on whether one of # the text fields has text or not self.navigationItem.rightBarButtonItem.enabled = (self.first_field.text.length + self.last_field.text.length > 0) end end

Persistance (the easy way)

There are lots of ways to persist data, we’re gonna go the super-stupid easy route and store our @players objects in a property list.

You can alternatively use a custom flat file (don’t!), an SQLite database using the embedded engine (this is good), Core Data (god help you learn it, and using it without Xcode is annoying), or NSUserDefaults, but then there are curve balls about read-only objects, and that method is really meant to store application settings, not application data.

The documents folder is stored here:

ruby docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]

Blech, I know. But there it is. Use BubbleWrap to make this more palatable. To get a specific file out of there, we use another verbose Cocoa method:

ruby filename = docs.stringByAppendingPathComponent("filename.txt")

GEEZ, Cocoa! Let’s make a Kernel method to do this for us. I put project-specific Kernel extensions in app_delegate.rb, you can put it wherever tickles your fancy. While we’re at it, I will add an exists method to make sure the file exists.

”`ruby class Kernel

def document(filename) @docs ||= NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] @docs.stringByAppendingPathComponent(filename) end

def exists(filename) NSFileManager.defaultManager.fileExistsAtPath(filename) end

end “`

DISCLAIMER: These names are short, and not namespaced, so I would not recommend just using this code in every project you write. Use BubbleWrap, it has methods like this.

Let’s add our persist and load methods to MyApplicationController:

”`ruby def plist document(‘editable_tableview.plist’) end

def persist @players.writeToFile(plist, atomically:true) end

def load_players if exists(plist) @players = NSArray.alloc.initWithContentsOfFile(plist) else @players = [ {‘first’ => ‘firsty’, ‘last’ => ‘McLasty’}, {‘first’ => ‘humphrey’, ‘last’ => ‘bogart’}, ] self.persist end end “`

And we need to call persist anytime the data changes, so I will add it to the playersChanged method, and I will add calls to that method in tableView(moveRowAtIndexPath:toIndexPath:) and tableView(commitEditingStyle:forRowAtIndexPath:).

”`ruby def playersChanged self.persist @table_view.reloadData end

def tableView(tableView, commitEditingStyle:editingstyle, forRowAtIndexPath:indexpath) # … self.playersChanged end

def tableView(tableView, moveRowAtIndexPath:fromindexpath, toIndexPath:toindexpath) # … self.playersChanged end “`

One quick note: When saving and restoring the @players array, the keys in each player Hash are written as Strings, rather than Symbols, which have no counterpart in Objective-C. So use strings, or write something that translates between your objects and the Foundation Framework classes.

If you run the app, add, edit, remove players, then quit (don’t just go home and back, you’re still running the app in memory), the changes should all be persisted nicely now. Whoopity-doo!

Like a Boss

Now let’s make this thing look boss-like. A plain, unstyled table view is fine, but it is so easy to do app-wide styles using the UIAppearance class, you should be asking yourself "why not?”. With just a couple images, and a few lines of code, we’ll end up with this:

  • tableview_tintcolor.png

    Tableview Tintcolor Png

Or this!

  • tableview_backgroundimage.png

    Tableview Backgroundimage Png


The first technique is just one line:

ruby UINavigationBar.appearance.tintColor = UIColor.colorWithRed(0.3, green:0.0, blue:0.5, alpha: 0) # fushcia

In real apps, I always use sugarcube, so this would be more like:

ruby UINavigationBar.appearance.tintColor = :fuschia.uicolor

Background image… for BarMetrics?

To set the background image you will need to use a bar metric, which just specifies whether the view is in landscape or portrait. Here’s the image I’m using:

  • tableview_background.png

    Tableview Background Png

Set it using the appearance delegate again:

ruby UINavigationBar.appearance.setBackgroundImage(UIImage.imageNamed("table-bg"), forBarMetrics: UIBarMetricsDefault)

You can, and probably should, set the tint when using a custom background, but if you really want to, you can create images for the buttons, too. They have to be very particular sizes, and you’ll need to create resizable images. AND you will need four images in total: button, button pressed, back button, back button pressed. Honestly, you should just set the tint and be done with it, let the default drawing do it’s thing.

But, since I’m sure you’re wondering:

  • tableview_btn_back_pressed.png

    Tableview Btn Back Pressed Png

  • tableview_btn_back.png

    Tableview Btn Back Png

  • tableview_btn_button_pressed.png

    Tableview Btn Button Pressed Png

  • tableview_btn_button.png

    Tableview Btn Button Png

ruby back_normal = UIImage.imageNamed("btn-back").resizableImageWithCapInsets([0, 14, 0, 6]) back_highlighted = UIImage.imageNamed("btn-back-pressed").resizableImageWithCapInsets([0, 14, 0, 6]) bar_button_normal = UIImage.imageNamed("btn-button").resizableImageWithCapInsets([0, 5.5, 0, 5.5]) bar_button_highlighted = UIImage.imageNamed("btn-button-pressed").resizableImageWithCapInsets([0, 5.5, 0, 5.5]) UIBarButtonItem.appearance.setBackButtonBackgroundImage(back_normal, forState: UIControlStateNormal, barMetrics: UIBarMetricsDefault) UIBarButtonItem.appearance.setBackButtonBackgroundImage(back_highlighted, forState: UIControlStateHighlighted, barMetrics: UIBarMetricsDefault) UIBarButtonItem.appearance.setBackgroundImage(bar_button_normal, forState: UIControlStateNormal, barMetrics: UIBarMetricsDefault) UIBarButtonItem.appearance.setBackgroundImage(bar_button_highlighted, forState: UIControlStateHighlighted, barMetrics: UIBarMetricsDefault)

  • tableview_background_allimages_1.png

    Tableview Background Allimages 1 Png

  • tableview_background_allimages_2.png

    Tableview Background Allimages 2 Png