ColorController will be a bit more involved than a text field and one button. The controller’s view will have two sections: a lower-half UITableView to list the color’s Tags and an upper section with an entry field to add new tags.
When we want to add a new tag, we send a POST request to Colr with the tag’s text. When that request finishes, we completely refresh our data and update the UI so the user knows the new tag is saved. POST requests are done just like GETs, except using HTTP.post. That sounds like a lot, so let’s take it one step at a time.
First we’re going to use a custom initializer for ColorController. This will take a Color as its sole argument, which makes sense given that the controller should always be associated with a color. In color_controllerrb, start our class like this:
| class ColorController < UIViewController |
| attr_accessor :color |
| def initWithColor(color) |
| initWithNibName(nil, bundle:nil) |
| self.color = color |
| self |
| end |
As we covered in Adding a New UIViewController, when writing an iOS initializer, you need to do two things: call the designated initializer and return self at the end. We’ll store the color using an instance variable @color, which attr_accessor uses to create the corresponding getter and setter.
Next we need to lay out the interface. We’re going to split the viewDidLoad method into two parts: one for the upper add-tag section and one for the tags table view. Brace yourself—lots of frame code is coming.
| def viewDidLoad |
| super |
| self.title = self.color.hex |
| self.edgesForExtendedLayout = UIRectEdgeNone |
| self.view.backgroundColor = UIColor.whiteColor |
| |
| padding = 10 |
| |
| @info_container = UIView.alloc.initWithFrame( |
| [[0, 0], [self.view.frame.size.width, 60]]) |
| @info_container.backgroundColor = UIColor.lightGrayColor |
| self.view.addSubview(@info_container) |
| |
| box_size = @info_container.frame.size.height - 2*padding |
| @color_view = |
| UIView.alloc.initWithFrame([[padding, padding], [box_size, box_size]]) |
| @color_view.backgroundColor = String.new(self.color.hex).to_color |
| self.view.addSubview(@color_view) |
| |
| text_field_origin = [ |
| @color_view.frame.origin.x + @color_view.frame.size.width + padding, |
| @color_view.frame.origin.y] |
| @text_field = UITextField.alloc.initWithFrame(CGRectZero) |
| @text_field.placeholder = "tag" |
| @text_field.autocapitalizationType = UITextAutocapitalizationTypeNone |
| @text_field.borderStyle = UITextBorderStyleRoundedRect |
| @text_field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter |
| self.view.addSubview(@text_field) |
| |
| @add = UIButton.buttonWithType(UIButtonTypeSystem) |
| @add.setTitle("Add", forState:UIControlStateNormal) |
| @add.setTitle("Adding...", forState:UIControlStateDisabled) |
| @add.setTitleColor(UIColor.lightGrayColor, forState:UIControlStateDisabled) |
| @add.sizeToFit |
| @add.frame = [ |
| [self.view.frame.size.width - @add.frame.size.width - padding, |
| @color_view.frame.origin.y], |
| [@add.frame.size.width, @color_view.frame.size.height]] |
| self.view.addSubview(@add) |
| add_button_offset = @add.frame.size.width + 2*padding |
| @text_field.frame = [ |
| text_field_origin, |
| [self.view.frame.size.width - text_field_origin[0] - add_button_offset, |
| @color_view.frame.size.height]] |
Whew, that is a lot of code, isn’t it? This is where Interface Builder comes in handy and reduces a lot of the complex calculations, like setting the frame of @text_field only after @add is completed. At this point, I think we know enough about how UIViews work for you to make your own choice about when to use IB and when to do it all in code.
Now that we have some our UI set up, let’s take it for a spin. We need to go back to SearchController and fix that open_color to use a ColorController.
| def open_color(color) |
» | controller = ColorController.alloc.initWithColor(color) |
» | self.navigationController.pushViewController(controller, animated:true) |
| end |
If we rake, you should see a nice upper bar, like so:
Just imagine that we have some slick gradients and 1-pixel shadows.
Next we need to add our UITableView of tags. After all of our other view creation, end viewDidLoad by creating the table view and making the controller its dataSource.
| table_height = self.view.bounds.size.height - @info_container.frame.size.height |
| table_frame = [[0, @info_container.frame.size.height], |
| [self.view.bounds.size.width, table_height]] |
| @table_view =UITableView.alloc.initWithFrame(table_frame, |
| style: UITableViewStylePlain) |
| @table_view.autoresizingMask = UIViewAutoresizingFlexibleHeight |
| self.view.addSubview(@table_view) |
| @table_view.dataSource = self |
| end |
Since we’re now done with the dataSource, we need to add the requisite methods to the controller. And since we already have the array we need with self.color.tags, our methods can be pretty lean.
| def tableView(tableView, numberOfRowsInSection:section) |
| self.color.tags.count |
| end |
| def tableView(tableView, cellForRowAtIndexPath:indexPath) |
| @reuseIdentifier ||= "CELL_IDENTIFIER" |
| cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) |
| cell ||= |
| UITableViewCell.alloc.initWithStyle( |
| UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier) |
| cell.textLabel.text = self.color.tags[indexPath.row].name |
| cell |
| end |
I told you it wasn’t very hard, didn’t I? Let’s try rake again and see how our tags look.
Now we get to the fun part: running POST requests to add new tags to a color.
Now that our UI is complete, it’s time to implement adding new tags. Just like we created a Color.find method, we’re going to add a Color#add_tag method to keep our API requests out of the controllers.
Let’s add our new method in colorrb. All of BubbleWrap’s HTTP methods allow a hash payload argument. For GET requests, this correctly appends the hash’s keys and values as a URL query; for POST/PUT/DELETE, they are added into the request body. Our implementation should look something like this:
| def add_tag(tag, &block) |
| BubbleWrap::HTTP.post("http://www.colr.org/js/color/#{self.hex}/addtag/", |
| payload:{tags: tag}) do |response| |
| if response.ok? |
| block.call(tag) |
| else |
| block.call(nil) |
| end |
| end |
| end |
Just like find, we use the block argument for callbacks. However, this time we use the ok? method of the response to detect whether everything worked, which checks for non-200 status codes.
Our block takes one argument, returning nil if the request fails. Let’s hook this up into the callback for the @add button in ColorController.
| self.view.addSubview(@add) |
| |
| @add.when(UIControlEventTouchUpInside) do |
| @add.enabled = false |
| @text_field.enabled = false |
| self.color.add_tag(@text_field.text) do |tag| |
| if tag |
| refresh |
| else |
| @add.enabled = true |
| @text_field.enabled = true |
| @text_field.text = "Failed :(" |
| end |
| end |
| end |
Not too bad, right? We remember to disable our UI while the request runs, which prevents the user from entering multiple new tags and allowing for weird race conditions. If add_tag works, we call this refresh method that we haven’t implemented yet. refresh will sync our Color to the current server data and then refresh the tags table so we see that everything worked. We can reuse our Color.find method, which looks pretty handy right now, doesn’t it?
| def refresh |
| Color.find(self.color.hex) do |color| |
| self.color = color |
| @table_view.reloadData |
| |
| @add.enabled = true |
| @text_field.enabled = true |
| end |
| end |
Fantastic, we’re done! One topic we didn’t discuss was how to test this app. RubyMotion’s framework gives you the building blocks for testing basic code, but testing dynamic features like HTTP requests makes things tricky. Thankfully, there are third-party libraries that can help you quickly test these more complex components. For example, WebStub (https://github.com/mattgreen/webstub) is a RubyMotion gem that allows you to transparently configure the data returned from any URLs your app requests. This is especially useful if you want to confirm the behavior for when things go wrong and erroneous data is returned from the server.
Our little Colr app may not have used the most popular API, but it’s a good example of how to structure any API-based app. And even if you aren’t interacting with a complex JSON API, BubbleWrap’s HTTP wrapper might be useful for more generic requests.
Now that you’ve built an app from start to more or less finish, it’s time to ship the final product to your users. Let’s dig into how we prepare and submit our creation to the App Store.