Ruby Motionチュートリアルやってみた(10)


Ruby Motionチュートリアルをやってます。前回の記事はこちら。

Ruby Motionチュートリアルはこちら。

▼開発環境
Ruby 2.5.3
Xcode 10.1
Ruby Motion 5.16

今回の章では、APIドリブン(カラー)の例を見ながら勉強を進めていきます。

この章も長くなるので、気合いを入れていきましょう!

大まかな概要は、検索のためのコントローラと色のためのコントローラを作成していくことになるそうです。そんで、それらはUINavigationControllerの中に書かれていくそうですよ。

あとは、Colorモデル、Tagモデルっていうモデルも必要になるんだって。

とりあえず、セットアップから始めましょう!

$ motion create Colr
$ cd Color
$ mkdir app/models
$ mkdir app/controllers

Rakefileにrequire 'bubble-wrap'とrequire 'bubble-wrap'を追記します。./appの配下にmodelsとcontrollersディレクトリを作成します。

まず、モデルについて見ていきます。Colr APIはJSONオブジェクトの形式で自身の色を返します。

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

ちなみに、tagsはTagオブジェクトのhas-manyの関係を表しているそうです。

じゃあ、実際にモデルを記述していきましょう!app/models/color.rbにコードを記述していきます。

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
 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メソッドを見てみると、ちょっと工夫されていることに気がつきますね!というのも、tagsにまだ値が与えられていない場合でも配列を返すことを保証しているのですね。またセッターではtagsに含まれる全てのオブジェクトがTagオブジェクトであることを保証しています。

それでは、いよいよTagクラス(モデル)を記述していきましょう。

app/models/tag.rbを作成します。APIから返されたタグの形式は次の通りです。

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

では、実際にコードを記述していきます。

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ディレクトリに、seach_controller.rbとcolor_controlle.rbを作成しましょう。

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

UIWindowとUINavigationControllerをAppDelegateを使って画面上に表示させましょう。

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

では、ここまでで一旦実行してみましょう。

いい感じですね♪では、SeachControllerを実装して行きましょう!

ユーザーが入力した文字を検索するのに、UITextFieldを使うそうです。

なんか、ユーザが検索ボタンを押すと、適切なAPIリクエストを実行し、それが終了するまでUIをロックするそうです。

結果を見つけた場合、ColorControllerをプッシュするそうです。

 def viewDidLoad
   super
   self.title = "Search"
   self.view.backgroundColor = UIColor.whiteColor
   @text_field = UITextField.alloc.initWithFrame [[0,0], [160, 26]]
   @text_field.placeholder = "検索..."
   # @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

個人的にチュートリアルからデザインをちょっとだけ変えときました。

一旦、実行して表示内容を確認してみます。

UIControlStateDisabledは、もしボタンが「@search.enabled = false」の場合にどのように見えるかを示しており、UITextBorderStyleRoundedRectとは、UITextFieldのスタイルを決定しています。

さて、次はイベントの設定をやって行きましょう!

 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要素を無効にするそうです。

次に、Color.findメソッドを調べましょう。URLリクエストはコントローラではなく、モデルの中で扱うようにした方がいいですよね。

ということで、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

ここではどんなことをしているのか、チュートリアルの説明を引用しておきます。

基本的な HTTP.get を使って,API URL 経由でサーバーからデータを取得します.&block 表記を使うことで,この関数がブロックと一緒に使われることを明確に表すようにしています.このブロックは別の引数として明示的に渡されるわけではなく,メソッドの後に do/end を書くことで暗黙的に渡されます..call(some, variables) の変数の数と順序は do |some, variables| に対応しています.

じゃ、早速適当な色を渡してみます。"3B5998"を渡してみます。

んー??なんか、to_strがエラーだって?というか、response自体がnilになってるっぽいな...。

ちょっと、原因究明。そもそも、BW::HTTP.getはちゃんと値を取得できているんだろうか?とりあえず、コンソールで実験。

いや、ちゃんと取得できてないじゃん。。。なんか、セキュリティ的に安全じゃないからAPIを使った取得をブロックしてるみたい。

んで、エラーメッセージによると、「Info.plist」ってので一時的に例外を記述できるらしい。

んで、Info.plistってファイル探すと3つあるっぽい。。。どれに、何を記述すればいいんだよー(:_;)

とりあえず、ググる!

このサイトの他にも色々調べてみたんだけど、わからん〜(;_;)

仕方ない、、、。保留にしますか。

と思ったけど、ここから先がデバッグできなくなっちゃった。。。

消化不良だけど、一旦ここで終わります。

まぁ、引き続きiPhone開発の勉強はして行くので、対策がわかったら更新するようにしますね!

では、また!

まとめ
ふー,大きな例ですね.ちゃんと設計がなされており,モデルとコントローラ間の責任を分離する一つの方法を示しています.ビューで様々なことをしたり,KVO を追加したり,もっと色々なことができたでしょうが,この程度の例では,それはやり過ぎだと思います.

この例から何を学びましょうか?

データを表現するのに,JSON.parse で得られるハッシュを直接使わず,モデルを使います.
モデルの中で URL リクエストを実行します.
コントローラを,コー​​ルバックとユーザーイベントに応答するのに使います.
時間のかかるリクエストが実行されている間,UIを無効にしたり変更して,インターフェイスの応答性を保ちます.

サポートしていただけると、泣いて喜びます! 嬉しくて仕事をめちゃめちゃ頑張れます。