Making GET Requests and SearchController – RubyMotion

Making GET Requests and SearchController

The first controller the user interacts with is the SearchController. Here, the user can enter a hexadecimal color code like #3B5998 and tap a button to see detailed information about that color. The only edge case we need to worry about is if the Colr database doesn’t contain that color; in that case, we’ll alert the user accordingly.

Setting Up the UI

How does the user enter text? We’re going reach back all the way to Making Text Dynamic with UITextField and use the venerable UITextField. We’ll use it along with a UIButton to actually trigger the search.

Let’s start setting up these elements. Open search_controllerrb and fill in viewDidLoad with our new subviews.

 class​ SearchController < UIViewController
 def​ viewDidLoad
 super
  self.title = ​"Search"
  self.view.backgroundColor = UIColor.whiteColor
 
  @text_field = UITextField.alloc.initWithFrame [[0,0], [160, 26]]
  @text_field.placeholder = ​"#abcabc"
  @text_field.textAlignment = UITextAlignmentCenter
  @text_field.autocapitalizationType = UITextAutocapitalizationTypeNone
  @text_field.borderStyle = UITextBorderStyleRoundedRect
  @text_field.center = [
  self.view.frame.size.width / 2,
  self.view.frame.size.height / 2 - 100]
  self.view.addSubview(@text_field)
 
  @search = UIButton.buttonWithType(UIButtonTypeSystem)
  @search.setTitle(​"Search"​, forState​:UIControlStateNormal​)
  @search.setTitle(​"Loading"​, forState​:UIControlStateDisabled​)
  @search.sizeToFit
  @search.center = [
  self.view.frame.size.width / 2,
  @text_field.center.y + 40]
  self.view.addSubview(@search)
 end
 end

Most of this is just specific positioning and sizing stuff that’s old-hat by now. One new bit is UIControlStateDisabled, which corresponds to what a control looks like if we set enabled to false. UITextBorderStyleRoundedRect is one style to make UITextFields look nice without any additional configuration, but StyleNone and StyleLine also exist.

Before we run our app, we need to set up our AppDelegate to use this controller. As we’ve done many times before, create a UINavigationController with our SearchController at its root.

 class​ AppDelegate
 def​ application(application, didFinishLaunchingWithOptions​:launchOptions​)
  @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
  @window.backgroundColor = UIColor.whiteColor
 
  @search_controller = SearchController.alloc.initWithNibName(​nil​, bundle​:nil​)
  @navigation_controller =
  UINavigationController.alloc.initWithRootViewController(@search_controller)
 
  @window.rootViewController = @navigation_controller
  @window.makeKeyAndVisible
 true
 end
 end

At this point, our Colr app should look like this:

Sadly, it doesn’t do a whole lot yet. Let’s start filling in that implementation with some callbacks.

Running GET Requests

Up to this point, we’ve detected UIButton taps with addTarget:action:forControlEvents:. It works, but having an external callback method passed by name is an awfully un-Ruby thing to do. BubbleWrap has a nifty little when wrapper to pass detect control events in a block, which makes our code much more readable. In viewDidLoad, add one of these blocks for our @search button:

 self.view.addSubview(@search)
 
 @search.when(UIControlEventTouchUpInside) ​do
  @search.enabled = ​false
  @text_field.enabled = ​false
 
  hex = @text_field.text
 # chop off any leading #s
  hex = hex[1..-1] ​if​ hex[0] == ​"#"
 
  Color.find(hex) ​do​ |color|
  @search.enabled = ​true
  @text_field.enabled = ​true
 end
 end

The when function is available to every UIControl (of which UIButton is a subclass) and takes the usual bitmask of UIControlEvents as its arguments. While the request runs, we temporarily disable our UI elements using the enabled property of each element.

But wait, where did that Color.find method come from? You should keep all of your URL requests inside of models instead of controllers. It keeps your controllers leaner, and if we want to grab a Color from the server somewhere else in the app, then there’s no copy-paste code duplication. And who knows, maybe we’ll need to do that too...foreshadowing.

It’s time to add that find static method to our Color. We’re finally going to play with HTTP requests here, starting with HTTP.get.

 def​ self.find(hex, &block)
  BubbleWrap::HTTP.get(​"http://www.colr.org/json/color/​​#{​hex​}​​"​) ​do​ |response|
  p response.body.to_str
  block.call(​nil​)
 end
 end

Not too bad, right? We use HTTP.get to send a GET request to the server via the correct API URL. Note that we use the block argument to make it plain that this method is intended to be used with a block. If you’re fuzzy on this part of Ruby, it means block isn’t explicitly passed as another argument but rather implicitly when we add a do/end after calling the method. The number and order of variables in block.call(some, variables) correspond to their representations in do |some, variables|.

Go ahead and rake and test it with a color like ff00ff. You should see something like this output in the console:

 (main)> "{\"colors\": [
  {\"timestamp\": 1285886579, \"hex\": \"ff00ff\", \"id\": 3976,
  \"tags\": [
  {\"timestamp\": 1108110851, \"id\": 2583, \"name\": \"fuchsia\"},
  {\"timestamp\": 1108110864, \"id\": 3810, \"name\": \"magenta\"},
  {\"timestamp\": 1108110870, \"id\": 4166, \"name\": \"magic\"},
  {\"timestamp\": 1108110851, \"id\": 2626, \"name\": \"pink\"},
  {\"timestamp\": 1240447803, \"id\": 24479, \"name\": \"rgba8b24ff00ff\"},
  {\"timestamp\": 1108110864, \"id\": 3810, \"name\": \"magenta\"}
  ]}]
  ...
 }"

That looks an awful lot like JSON, doesn’t it? That makes sense, given the /json/ in the URL. Wouldn’t it be convenient if we could parse that into a normal Ruby hash and use it to instantiate our hash-friendly models?

BubbleWrap to the rescue again! Our nifty friend has a BubbleWrap::JSON.parse method, which takes a JSON string and spits out a Ruby hash. Let’s update Color.find to use it.

 def​ self.find(hex, &block)
  BubbleWrap::HTTP.get(​"http://www.colr.org/json/color/​​#{​hex​}​​"​) ​do​ |response|
» result_data = BubbleWrap::JSON.parse(response.body.to_str)
» color_data = result_data[​"colors"​][0]
»
»# Colr will return a color with id == -1 if no color was found
»
» color = Color.new(color_data)
»if​ color.id.to_i == -1
» block.call(​nil​)
»else
» block.call(color)
»end
 
 end
 end

Nice and easy. Our edge-case comes into play here when the only color returned has an id of -1, meaning no results were found. We need to update our SearchController to deal with that and the normal, successful case.

 Color.find(hex) ​do​ |color|
»if​ color.nil?
» @search.setTitle(​"None :("​, ​forState: ​UIControlStateNormal)
»else
» @search.setTitle(​"Search"​, ​forState: ​UIControlStateNormal)
» self.open_color(color)
»end
  @search.enabled = ​true
  @text_field.enabled = ​true
 end
»def​ open_color(color)
» p ​"Opening ​​#{​color.inspect​}​​"
»end

For now, we just print out the returned Color object upon a successful search. If you run the app now and search a color, the console should display all the properties of the Color in addition to the related Tag objects. Pretty neat, right? But now we need to change open_color(color) to its final implementation with a ColorController class. I guess now is a good time to create that controller.