RubyMotion 指南:API 驅動開發示例

翻譯:@shiweifu
本文連接:http://segmentfault.com/blog/shiweifu
原文連接:http://rubymotion-tutorial.com/10-api-driven-example/
目標讀者:["想了解RubyMotion開發模式", "想學習RubyMotion", "逗比"]html


咱們將建立一個使用Colr JSON API做爲後端的應用。用戶輸入顏色的16進制值( #3B5998)他們會看見標籤的顏色發生對應的變化。他們能夠往裏添加新的顏色。git

咱們先考慮下程序的結構。會有兩個Controller:一個用來搜索,一個用來顯示顏色。這兩個Controller外面都套着UINavigationController。咱們還須要ModelColorTag,它可能並不精美,但能工做。github

初始化

使用motion create Colr 命令初始化一個新的項目,添加bubble-wrap 到你的 Rakefile。接下來咱們在./app 中建立兩個目錄:./app/models/./app/controllersjson

Models

首先,讓咱們先看下模型。Colr API 的 Color JSON 結構以下:segmentfault

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

咱們的 Colors 須要timestamphexidtags這些屬性,特別注意的是,tags屬性將包含多個Tag對象後端

建立./app/models/color.rb而後填寫 Model 代碼:api

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

  ...

PROPERTIES 這塊是個小trick,很容易就定義了屬性。須要稍微說一下的是tags這個屬性,讓它始終返回一個Tag Model的數組。數組

...

  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

咱們覆蓋了#tags 的getter和setter,因此當tags沒有值的時候,將返回一個空的數組。#tags=保證解析和返回Tag對象數組。咱們接下來編看看TagModel裏面都有啥。ruby

建立並打開./app/models/tag.rb,接口返回的數據以下所示:服務器

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

建立TagModel的類,代碼短且友好:

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

模型都已經定義好了,你的好友「控制器君」即將上線。建立./app/controllers/search_controller.rb./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

將咱們的控制器帶上UINavigationControllerUIWindow,甩給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

代碼堆砌完了,是時候看當作果了,執行rake命令,在屏幕中會出現:

一切都很好,該看看SearchController裏面都有啥了。

SearchController

(譯者著:原文是系列文章,以前的部分從未出現過UITextField,因此這裏假設UITextField從未出現過,否則接不上。)

咱們將使用一個以前從未提到過的控件UITextField來接受用戶的輸入,當用戶點擊Search按鈕時候,咱們將發起一個API請求,這時界面不接受任何輸入,直到請求結束。若是請求成功完成,咱們會push一個ColorController給用戶展現結果,不然的話會給個出錯提示。

如下是SearchController的初始化時幹活的代碼:

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 = 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.size.height / 2 - 100設置座標和大小的代碼是我我的習慣,設置UIControlStateDisabled是爲了統一配置阻塞時的樣式。UITextBorderStyleRoundedRect是爲了設置UITexitField的樣式,帶來更好的觀感。

rake再執行,如今看到的樣子:

(譯者注:BubbleWrap是RubyMotion官方開發的一個庫,裏面封裝了不少用Cocoa寫起來很蛋疼的地方,使代碼更加「Ruby」)

該處理事件了。還記得我以前提到過BubbleWrap屌屌的麼?使用它咱們不用再像過去寫傻傻的addTarget:action:forControlEvents啥啥啥的來添加事件,代碼清晰不少:

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

when 方法在全部UIControl的子類均可以用。使用UIControlEvent開頭的那些標識事件位做爲參數。當請求發出後,咱們臨時禁用UI。

(譯者注:做者的意思應該是每一個顏色都寫一段代碼去獲取,有疑問去看原文吧,若是不是這樣,記得指正 T.T)
Color.find這個方法是哪來的?在這裏,咱們將 URL 處理的代碼放到模型裏,而不是放到控制器裏。當須要獲得一個Color對象的時候,只須要咱們傳遞個block進去,不用在控制器中去寫重複的代碼了。

Color類添加find類方法:

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。若是困惑或者想深刻研究,能夠去看看Ruby的lambda,還有RubyMotion的block傳遞)

有些困惑?咱們使用簡單的HTTP.get去請求服務器,獲得數據,而後經過&block傳出去。調用的時候,請求調用完畢後,會執行調用的時候do/end之間的代碼。經過.call(some, variables)執行do |some, variables|

rake一下,來個數據測試一下這個方法,如:3B5998。你將在終端中看到:

(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!!一坨JSON字符串啊,親我不想要字符串啊,能不能給我Ruby的Hash?

在BubbleWrap裏已經集成了解析JSON的方法:BW::JSON.parse,開箱即用:

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

在咱們的SearchController中,要作一些對無效輸入的校驗:

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

一切看起來很好。當遇到無效的JSON的時候界面上會給出明確的反饋:

如今改補上 open_color 方法的代碼了。它push一個ColorController,而後在其中顯示顏色。

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

ColorController

咱們要自定義ColorController的構造函數。這個Controller的視圖有兩部分:一個UITableView,用來顯示顏色標記,一個Section 顯示具體顏色和添加新的標記。當咱們想要標記一個顏色的時候,咱們要發一個請求,而後再刷新讓它顯示出來。

不嘴炮了,看看代碼:

class ColorController < UIViewController
  attr_accessor :color

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

  ...

當重載一個iOS SDK 構造函數的時候,你須要作兩件事:調用它的父構造函數;在函數結尾的時候返回初始化過的它本身。在RubyMotion中,你不能像標準Ruby同樣初始化。

初始化完畢,該佈局了:

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

……好大一坨代碼啊!不要慌,這些代碼很容易理解,咱們只是添加了幾個子view。

rake一下試試看?

image

額……真的很醜啊……

處理tags沒啥特別的,就是實現一個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

再次運行rake,有點意思了吧?

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

接下來要添加新的tags,有多種方法去實現。你能夠老老實實的Tag.create(tag),也可使用Ruby的黑魔法color.tags << tag,但爲了體現出Color和Tag存在聯繫,咱們這麼作:color.add_tag(tag, &block)

這個方法實現以下::

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

最後那個參數是在請求執行結束後回調的。好的作法是分別處理成功和失敗兩種狀況,這個例子爲了簡單,就先不考慮了。

如今給ColorController的按鈕添加事件處理代碼。咱們想在Tag被髮送到服務器以後,根據當前服務器返回的數據刷新:

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

咱們給@add按鈕添加了UIControlEventTouchUpInside事件,在事件觸發的時候,會POST添加請求給服務器。當請求處理結束,咱們刷新頁面。這將觸發Color.find,重設咱們的數據。

rake一下,添加tag試試?

時候到溜

這片冗長的教程終於要結束了。在教程中,咱們分離了ControllerModel,由於要保持示例足夠小,沒怎麼考慮View,若是要考慮View,就須要引入KVO或相似的技術。做爲預覽,本文的示例已經足夠給力了。

到底講了點啥?

  • 使用Model處理你的JSON數據,而不是使用DictionaryHash
  • 把請求放到了Model
  • Controller 響應用戶事件
  • 在請求執行過程當中,阻塞界面
相關文章
相關標籤/搜索