RubyMotion 指南:API 驱动开发示例
翻譯:@shiweifu
本文鏈接:http://segmentfault.com/blog/shiweifu
原文鏈接:http://rubymotion-tutorial.com/10-api-driven-example/
目標讀者:["想了解RubyMotion開發模式", "想學習RubyMotion", "逗比"]
我們將創建一個使用Colr JSON API作為后端的應用。用戶輸入顏色的16進制值( #3B5998)他們會看見標簽的顏色發生對應的變化。他們可以往里添加新的顏色。
我們先考慮下程序的結構。會有兩個Controller:一個用來搜索,一個用來顯示顏色。這兩個Controller外面都套著UINavigationController。我們還需要Model:Color、Tag,它可能并不精美,但能工作。
初始化
使用motion create Colr 命令初始化一個新的項目,添加bubble-wrap 到你的 Rakefile。接下來我們在./app 中創建兩個目錄:./app/models/ 和 ./app/controllers。
Models
首先,讓我們先看下模型。Colr API 的 Color JSON 結構如下:
{"timestamp": 1285886579,"hex": "ff00ff","id": 3976,"tags": [{"timestamp": 1108110851,"id": 2583,"name": "fuchsia"}] }我們的 Colors 需要timestamp,hex, id, tags這些屬性,特別注意的是,tags屬性將包含多個Tag對象
創建./app/models/color.rb然后填寫 Model 代碼:
class ColorPROPERTIES = [:timestamp, :hex, :id, :tags]PROPERTIES.each { |prop|attr_accessor prop}def initialize(hash = )hash.each { |key, value|if PROPERTIES.member? key.to_symself.send((key.to_s + "=").to_s, value)end}end...PROPERTIES 這塊是個小trick,很容易就定義了屬性。需要稍微說一下的是tags這個屬性,讓它始終返回一個Tag Model的數組。
...def tags@tags ||= []enddef tags=(tags)if tags.first.is_a? Hashtags = tags.collect |tag| Tag.new(tag) endtags.each { |tag|if not tag.is_a? Tagraise "Wrong class for attempted tag #tag.inspect"end}@tags = tagsend end我們覆蓋了#tags 的getter和setter,所以當tags沒有值的時候,將返回一個空的數組。#tags=保證解析和返回Tag對象數組。我們接下來編看看TagModel里面都有啥。
創建并打開./app/models/tag.rb,接口返回的數據如下所示:
{"timestamp": 1108110851,"id": 2583,"name": "fuchsia" }創建TagModel的類,代碼短且友好:
class TagPROPERTIES = [:timestamp, :id, :name]PROPERTIES.each { |prop|attr_accessor prop}def initialize(hash = )hash.each { |key, value|if PROPERTIES.member? key.to_symself.send((key.to_s + "=").to_s, value)end}end endControllers
模型都已經定義好了,你的好友「控制器君」即將上線。創建./app/controllers/search_controller.rb和./app/controllers/color_controller.rb 倆文件,把最基本的實現先寫上去:
class SearchController < UIViewControllerdef viewDidLoadsuperself.title = "Search"end end class ColorController < UIViewControllerdef viewDidLoadsuperself.title = "Color"end end將我們的控制器帶上UINavigationController和UIWindow,甩給AppDelegate:
class AppDelegatedef 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.makeKeyAndVisibletrueend end代碼堆砌完了,是時候看看成果了,執行rake命令,在屏幕中會出現:
一切都很好,該看看SearchController里面都有啥了。
SearchController
(譯者著:原文是系列文章,之前的部分從未出現過UITextField,所以這里假設UITextField從未出現過,不然接不上。)
我們將使用一個之前從未提到過的控件UITextField來接受用戶的輸入,當用戶點擊Search按鈕時候,我們將發起一個API請求,這時界面不接受任何輸入,直到請求結束。如果請求成功完成,我們會push一個ColorController給用戶展示結果,否則的話會給個出錯提示。
以下是SearchController的初始化時干活的代碼:
def viewDidLoadsuperself.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 @searchendself.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 = falsehex = @text_field.text# chop off any leading #shex = hex[1..-1] if hex[0] == "#"Color.find(hex) do |color|@search.enabled = true@text_field.enabled = trueendendendwhen 方法在所有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)endend 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 foundcolor = Color.new(color_data)if color.id.to_i == -1block.call(nil)elseblock.call(color)endend 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 = trueendendenddef 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) endColorController
我們要自定義ColorController的構造函數。這個Controller的視圖有兩部分:一個UITableView,用來顯示顏色標記,一個Section 顯示具體顏色和添加新的標記。當我們想要標記一個顏色的時候,我們要發一個請求,然后再刷新讓它顯示出來。
不嘴炮了,看看代碼:
class ColorController < UIViewControllerattr_accessor :colordef initWithColor(color)initWithNibName(nil, bundle:nil)self.color = colorselfend...當重載一個iOS SDK 構造函數的時候,你需要做兩件事:調用它的父構造函數;在函數結尾的時候返回初始化過的它自己。在RubyMotion中,你不能像標準Ruby一樣初始化。
初始化完畢,該布局了:
def viewDidLoadsuperself.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.lightGrayColorself.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_colorself.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.sizeToFitself.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 = UITextBorderStyleRoundedRectself.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一下試試看?
額……真的很丑啊……
處理tags沒啥特別的,就是實現一個delegate。
def viewDidLoad...@table_view.dataSource = selfenddef tableView(tableView, numberOfRowsInSection:section)self.color.tags.countenddef tableView(tableView, cellForRowAtIndexPath:indexPath)@reuseIdentifier ||= "CELL_IDENTIFIER"cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) || beginUITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)endcell.textLabel.text = self.color.tags[indexPath.row].namecellend再次運行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.callendend最后那個參數是在請求執行結束后回調的。好的做法是分別處理成功和失敗兩種情況,這個例子為了簡單,就先不考慮了。
現在給ColorController的按鈕添加事件處理代碼。我們想在Tag被發送到服務器之后,根據當前服務器返回的數據刷新:
def viewDidLoad...self.view.addSubview(@add)@add.when(UIControlEventTouchUpInside) do@add.enabled = false@text_field.enabled = falseself.color.add_tag(@text_field.text) dorefreshendend...enddef refreshColor.find(self.color.hex) do |color|self.color = color@table_view.reloadData@add.enabled = true@text_field.enabled = trueendend我們給@add按鈕添加了UIControlEventTouchUpInside事件,在事件觸發的時候,會POST添加請求給服務器。當請求處理結束,我們刷新頁面。這將觸發Color.find,重設我們的數據。
rake一下,添加tag試試?
時候到溜
這片冗長的教程終于要結束了。在教程中,我們分離了Controller和Model,因為要保持示例足夠小,沒怎么考慮View,如果要考慮View,就需要引入KVO或類似的技術。作為預覽,本文的示例已經足夠給力了。
到底講了點啥?
- 使用Model處理你的JSON數據,而不是使用Dictionary或Hash
- 把請求放到了Model中
- Controller 響應用戶事件
- 在請求執行過程中,阻塞界面
總結
以上是生活随笔為你收集整理的RubyMotion 指南:API 驱动开发示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Jenkins 部署
- 下一篇: js简单的设置快捷键,hotkeys捕获