POST Requests and ColorController

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.

Setting Up the UI

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:

images/api_example/upper_half.png

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.

images/api_example/lower_half.png

Now we get to the fun part: running POST requests to add new tags to a color.

Running POST Requests

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset