Translation: @ shiweifu this article links: http://segmentfault.com/blog/shiweifu the original links: http://rubymotion-tutorial.com/10-api-driven-example/ target audience: [” Want to learn RubyMotion development mode “, “Want to learn RubyMotion”,” Toby “]


We will create an application that uses the Colr JSON API as the back end. Users enter the hexadecimal value of the color (#3B5998) and they will see the color of the tag change accordingly. They can add new colors to it.

Let’s think about the structure of the program. There will be two controllers: one for searching and one for displaying colors. Both of these controllers are covered with UINavigationControllers. We also need the Model: Color, Tag, which may not be pretty, but it works.

Initialize the

Use the motion create Colr command to initialize a new project and add bubble-wrap to your Rakefile. Next we create two directories in./app:./app/models/ and./app/controllers.

Models

First, let’s look at the model. The Color JSON structure of the Colr API is as follows:

{
  "timestamp": 1285886579,
  "hex": "ff00ff",
  "id": 3976,
  "tags": [{
    "timestamp": 1108110851,
    "id": 2583,
    "name": "fuchsia"
  }]
}

Our Colors will require attributes such as timestamp, hex, id, tags. Note that the tags attribute will contain multiple Tag objects

Create./app/models/color.rb and fill in the Model code:

class Color
  PROPERTIES = [:timestamp, :hex, :id, :tags]
  PROPERTIES.each { |prop|
    attr_accessor prop
  }

  def initialize(hash = )
    hash.each { |key, value|
      if PROPERTIES.member? key.to_sym
        self.send((key.to_s + "=").to_s, value)
      end
    }
  end

  ...

The Properties piece is a little trick. It’s easy to define the PROPERTIES. One thing that needs to be said about tags is that it always returns an array of Tag models.

. def tags @tags ||= [] end def tags=(tags) if tags.first.is_a? Hash tags = tags.collect |tag| Tag.new(tag) end tags.each { |tag| if not tag.is_a? Tag raise "Wrong class for attempted tag #tag.inspect" end } @tags = tags end end

We’ve overridden the getter and setter for #tags, so when tags have no value, we’ll return an empty array. Ensures that an array of Tag objects is parsed and returned. So let’s make up what’s in the TagModel.

Create and open./app/models/tag.rb, and the interface returns data like this:

{
  "timestamp": 1108110851,
  "id": 2583,
  "name": "fuchsia"
}

Create a TagModel class with short and friendly code:

class Tag
  PROPERTIES = [:timestamp, :id, :name]
  PROPERTIES.each { |prop|
    attr_accessor prop
  }

  def initialize(hash = )
    hash.each { |key, value|
      if PROPERTIES.member? key.to_sym
        self.send((key.to_s + "=").to_s, value)
      end
    }
  end
end

Controllers

The models are all defined, and your buddy, Controller King, is about to come online. Create./app/controllers/search_controller.rb and./app/controllers/color_controller.rb.

class SearchController < UIViewController
  def viewDidLoad
    super

    self.title = "Search"
  end
end
class ColorController < UIViewController
  def viewDidLoad
    super

    self.title = "Color"
  end
end

Take our controller with the UINavigationController and UIWindow and dump it to the AppDelegate:

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

    @search_controller = SearchController.alloc.initWithNibName(nil, bundle:nil)
    @navigation_controller = UINavigationController.alloc.initWithRootViewController(@search_controller)

    @window.rootViewController = @navigation_controller
    @window.makeKeyAndVisible
    true
  end
end

Now that you’ve finished piling up your code, it’s time to see the results. Execution the rake command will bring up the following screen:

Everything’s fine, so it’s time to see what’s in the SearchController.

SearchController

The UITextField has never been used in the previous part of this series, so assume that the UITextField has never been used.

We will use a control called UITextField that we have never mentioned before to accept input from the user. When the user clicks the Search button, we will make an API request, and the interface will accept no input until the request ends. If the request completes successfully, we will push a ColorController to show the result to the user, otherwise we will send an error message.

Here’s the code for initializing SearchController:

def viewDidLoad super self.title = "Search" self.view.backgroundColor = UIColor.whiteColor @text_field = UITextField at. 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 = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - 100) self.view.addSubview @text_field @search = UIButton.buttonWithType(UIButtonTypeRoundedRect) @search.setTitle("Search", forState:UIControlStateNormal) @search.setTitle("Loading", forState:UIControlStateDisabled) @search.sizeToFit @search.center = CGPointMake(self.view.frame.size.width / 2, @text_field.center.y + 40) self.view.addSubview @search end

Self. View. Frame. The size. Setting the height / 2-100 coordinates and the size of the code is my personal habits, set UIControlStateDisabled to unified configuration block style. UITextBorderStyleRoundedRect is to set the style of UITexitField, lead to a better look.

Rake executes again and now sees what it looks like:

BubbleWrap is a library developed by RubyMotion that encapsulates a lot of things that would be awkward to write in Cocoa and makes the code more “Ruby”.

It’s time to handle the event. Remember what I said about BubbleWrap? Using it we don’t have to like the past writing silly addTarget: action: forControlEvents what what what to add events, code clear a lot:

  def viewDidLoad
    ...

    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
  end

The when method is available in all subclasses of UIControl. Use those identifying event bits starting with UIControleEvent as parameters. When the request is made, we temporarily disable the UI.

Where does the color. find method come from? If you want to get a Color, you need to write a piece of code to get it. In this case, we put the URL processing code in the model, not in the controller. When we need to get a Color object, we just pass a block in, we don’t have to write duplicate code in the controller.

Add a find method to the Color class:

class Color
  ...

  def self.find(hex, &block)
    BW::HTTP.get("http://www.colr.org/json/color/#hex") do |response|
      p response.body.to_str
      # for now, pass nil.
      block.call(nil)
    end
  end
end

(Rubymotion block. If you’re confused or want to dig deeper, check out Ruby’s lambda and RubyMotion’s block passing.)

Confused? We use a simple http. get to request the server, get the data, and send it through the &block. Upon completion of the request, the code between do/end at the time of the call is executed. Through the call (some variables) perform the do | some, the variables |.

Rake again to test the method with data such as 3B5998. You will see in the terminal:

(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\"}]], \"schemes\": [], \"schemes_history\": , \"success\": true, \"colors_history\": \"ff00ff\": [{\"d_count\": 0, \"id\": \"4166\", \"a_count\": 1, \"name\": \"magic\"}, {\"d_count\": 0, \"id\": \"2626\", \"a_count\": 1, \"name\": \"pink\"}, {\"d_count\": 0, \"id\": \"24479\", \"a_count\": 1, \"name\": \"rgba8b24ff00ff\"}, {\"d_count\": 0, \"id\": \"3810\", \"a_count\": 1, \"name\": \"magenta\"}], \"messages\": [], \"new_color\": \"ff00ff\"}\n"

WTF!!!!! I don’t want a string. Can you give me a Ruby Hash?

BubbleWrap has integrated a method for parsing JSON: BW:: json.parse, out of the box:

def self.find(hex, &block)
  BW::HTTP.get("http://www.colr.org/json/color/#hex") do |response|
    result_data = BW::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

In our SearchController, we need to do some validation for invalid input:

def viewDidLoad
    ...

      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
    end
  end

  def open_color(color)
    p "Opening #color"
  end

Everything looks good. Fixed bug MC-115416 – The interface will give clear feedback when an invalid JSON is encountered:

Now the open_color method code is fixed. It pushes a ColorController, and it displays the color in it.

def open_color(color)
  self.navigationController.pushViewController(ColorController.alloc.initWithColor(color), animated:true)
end

ColorController

We’re going to customize the constructor of the ColorController. The Controller’s view has two parts: a UITableView to display color tags, and a Section to display specific colors and add new tags. When we want to mark a color, we make a request and then refresh it to show up.

Just look at the code:

class ColorController < UIViewController
  attr_accessor :color

  def initWithColor(color)
    initWithNibName(nil, bundle:nil)
    self.color = color
    self
  end

  ...

When overloading an iOS SDK constructor, you need to do two things: call its parent constructor; Returns the initialized itself at the end of the function. In RubyMotion, you can’t initialize like standard Ruby.

After initialization, the layout is ready:

def viewDidLoad super self.title = self.color.hex # You must comment out the following line if you are developing on iOS  < 7. self.edgesForExtendedLayout = UIRectEdgeNone # A light grey background to separate the Tag table from the Color info @info_container = UIView.alloc.initWithFrame [[0, 0], [self.view.frame.size.width, 110]] @info_container.backgroundColor = UIColor.lightGrayColor self.view.addSubview @info_container # A visual preview of the actual color @color_view = UIView.alloc.initWithFrame [[10, 10], [90, 90]] # String#to_color is another handy BubbbleWrap addition! @color_view.backgroundColor = String.new(self.color.hex).to_color self.view.addSubview @color_view # Displays the hex code of our color @color_label = UILabel.alloc.initWithFrame [[110, 30], [0, 0]] @color_label.text = self.color.hex @color_label.sizeToFit self.view.addSubview @color_label # Where we enter the new  tag @text_field = UITextField.alloc.initWithFrame [[110, 60], [100, 26]] @text_field.placeholder = "tag" @text_field.textAlignment = UITextAlignmentCenter @text_field.autocapitalizationType = UITextAutocapitalizationTypeNone @text_field.borderStyle = UITextBorderStyleRoundedRect self.view.addSubview @text_field # Tapping this adds the tag. @add = UIButton.buttonWithType(UIButtonTypeRoundedRect) @add.setTitle("Add", forState:UIControlStateNormal) @add.setTitle("Adding..." , forState:UIControlStateDisabled) @add.setTitleColor(UIColor.lightGrayColor, forState:UIControlStateDisabled) @add.sizeToFit @add.frame = [[@text_field.frame.origin.x + @text_field.frame.size.width  + 10, @text_field.frame.origin.y], @add.frame.size] self.view.addSubview(@add) # The table for our color's tags. table_frame = [[0, @info_container.frame.size.height], [self.view.bounds.size.width, self.view.bounds.size.height - @info_container.frame.size.height - self.navigationController.navigationBar.frame.size.height]] @table_view = UITableView.alloc.initWithFrame(table_frame, style:UITableViewStylePlain) self.view.addSubview(@table_view) end

… What a huge chunk of code! Don’t panic, the code is easy to understand, we just added a few child views.

Give it a try, Rake?

The forehead… It’s really ugly…

There’s nothing special about handling tags, just implementing a delegate.

def viewDidLoad
    ...

    @table_view.dataSource = self
  end

  def tableView(tableView, numberOfRowsInSection:section)
    self.color.tags.count
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    @reuseIdentifier ||= "CELL_IDENTIFIER"

    cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) || begin
      UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)
    end

    cell.textLabel.text = self.color.tags[indexPath.row].name

    cell
  end

So let’s run rake again. Is that interesting?

! ()[http://rubymotion-tutorial.com/10-api-driven-example/images/4.png]

The next step is to add new tags, and there are several ways to do that. You can use tag.create (Tag) in good sense, or you can use Ruby’s dark magic of color.tags << Tag, but to show the connection between color and Tag, let’s do this: color.add_tag(Tag, &block).

This method is implemented as follows:

  def add_tag(tag, &block)
    BW::HTTP.post("http://www.colr.org/js/color/#{self.hex}/addtag/", payload: {tags: tag}) do |response|
      block.call
    end
  end

The last parameter is called back after the request completes. It’s a good idea to deal with success and failure separately, but I’m going to leave this example out for simplicity.

Now add the event handling code to the button of the ColorController. After the Tag is sent to the server, we want to refresh it according to the data returned by the current server:

  def viewDidLoad
    ...

    self.view.addSubview(@add)

    @add.when(UIControlEventTouchUpInside) do
      @add.enabled = false
      @text_field.enabled = false
      self.color.add_tag(@text_field.text) do
        refresh
      end
    end

    ...
  end

  def refresh
    Color.find(self.color.hex) do |color|
      self.color = color

      @table_view.reloadData

      @add.enabled = true
      @text_field.enabled = true
    end
  end

We gave @ the add button to add the UIControlEventTouchUpInside event, the event is triggered, will add a POST request to the server. When the request is finished, we refresh the page. This will trigger Colour.find to reset our data.

Let’s rake, let’s add a tag, okay?

Time to sneak

This long tutorial is finally coming to an end. In the tutorial, we separated the Controller from the Model, because to keep the example small enough, we didn’t think much about the View, which would have required the introduction of KVO or similar technology. As a preview, the examples in this article are compelling enough.

What was it about?

  • useModelProcessing yourJSONData, not usageDictionaryorHash
  • I put the request inModelIn the
  • ControllerResponding to user events
  • Blocking the interface during the execution of the request