Models
これまでビューとコントローラを扱いましたが、MVC の M はどこへいってしまったのでしょう?これからモデルに突入するので、これ以上待つ必要はありません。私たちは VC パートですでにたくさんのコードを書いてきましたが、一般的にはモデルは本当は 3 つのうち一番巨大になります。
RubyMotion では、モデルのために 2 つ大きなコンポーネントがあります。CoreData とそれ以外のすべてです。CoreData は iOS 向けの ORM の一種で、Rails の ActiveRecord と同様のものです。CoreData は信じられないほど強力なフレームワークで、それ自体で章が成り立つほどです。ここでは、RubyMotion で重要な 「CoreData 以外のすべて」を扱います。それでは、さっそくはじめましょう。
Model-T
モデルは一般的な Ruby のオブジェクトです。インスタンス変数をセット・ゲットするメソッドを宣言する場合に、普通の Ruby の attr_[accessor/reader/writer]
を使用することができます。たとえば次のように基本的な User
オブジェクトを作ってみましょう。
class User
attr_accessor :id
attr_accessor :name
attr_accessor :email
end
これでオブジェクトを次のように操作することができます。
@user = User.new
@user.name = "Clay"
@user.email = "clay@mail.com"
クールですがリモート API などを扱う場合に、取得するいくつかのデータからおそらく User
をパースすることになるでしょう。これを行う方法はいつくかありますが、個人的に次の方法がお気に入りです。
class User
PROPERTIES = [:id, :name, :email]
PROPERTIES.each { |prop|
attr_accessor prop
}
def initialize(attributes = {})
attributes.each { |key, value|
self.send("#{key}=", value) if PROPERTIES.member? key.to_sym
}
end
end
これで、次のような巧妙なトリックを行うことができます。
server_hash = { name: "Clay", email: "my@email.com", id: 1000 }
@user = User.new(server_hash)
@user.name
=> Clay
このスタイルは、単に定数 PROPERTIES
を拡張することでより多くのプロパティを簡単に追加できます。モデルをシリアライズしたり、モデルの属性を何度も処理する必要がある場合にも便利です。あぁ、失礼、これは話の伏線ですね。
NSCoding
モデル用のスケーラブルな基盤があり、それを使ってサーバからデータを取得することができます。データを取得しようとするたび API を呼び出す代わりに、まずローカルのコピー (別名: キャッシュ) があるかをチェックしたいと思います。オブジェクトをファイルなどに保存するコードを作成することもできますが、もっと良いアイディアがあります。
永続的な key-value ストアとして NSUserDefaults
というオブジェクトがあります。通常、オブジェクトとして NSUserDefaults.standardUserDefaults
のインスタンスを次のように使用することができます。
@defaults = NSUserDefaults.standardUserDefaults
@defaults["one"] = 1
# 別の場所で、あるいは、アプリを起動し直しても値を取得できます
@defaults["one"]
=> 1
とても良さそうでしょう? NSUserDefaults
の値はアプリがインストールされているあいだ保存されます。値を急いで処理したい場合には、NSUserDefaults.resetStandardUserDefaults
を実行することで全てのエントリを消去できます。
ここで注意があります。NSUserDefaults
には古いオブジェクトを格納することはできません。プリミティブな値(string, integer, hash など)の格納や生の data/byte 列 は使用することができます。User
オブジェクトはプリミティブではないため、data メソッドを使う必要があります。どのように動作するのでしょうか?
NSKeyedArchiver
を使用しアーカイブ処理を行うことでモデルを配置できます。この NSKeyedArchiver
クラスはオブジェクトを受けつけ NSUserDefaults
で保存できる NSData
のインスタンスを作ります。アーカイブできるオブジェクトは NSCoding
に準拠します。つまり、標準的な API を使用し自身をシリアライズ・デシリアライズする方法を定義する 2 つのメソッドを実装することを意味します。もしモデルがこれらのメソッドを実装していない場合、アーカイブすることができません。
NSCoding
に準拠するための 2 つのメソッドは initWithCoder:
(オブジェクトを読み込むために使われます) と encodeWithCoder:
(オブジェクトを保存するために使われます) です。これら両方のメソッドには NSCoder
のインスタンスが渡され、与えられたキーでプリミティブなオブジェクトをエンコードします。NSKeyedArchiver
と NSKeyedUnarchiver
はアーカイブされたオブジェクトを保存形式に変換したり、逆に保存形式からアーカイブされたオブジェクトに変換するためにさきほどのメソッドを使用します。
2 つのメソッドはよく似た並びにすべきでしょう。encodeWithCoder
はいくつかのキーに対してオブジェクトの値のすべてを エンコード し、それから initWithCoder
は エンコード時と同じキー でオブジェクトの値を セット します。
さぁ、例を見てみましょう。二つのメソッドは基本的に正反対のことを行うことに注意してください。
class Post
attr_accessor :message
attr_accessor :id
# NSUserDefaults からオブジェクトがロードされる時に呼び出されます。
# イニシャライザなので、`self` を返さなければなりません。
def initWithCoder(decoder)
self.init
self.message = decoder.decodeObjectForKey("message")
self.id = decoder.decodeObjectForKey("id")
self
end
# NSUserDefaults へオブジェクトを保存するときに呼び出されます。
def encodeWithCoder(encoder)
encoder.encodeObject(self.message, forKey: "message")
encoder.encodeObject(self.id, forKey: "id")
end
end
それではいったい何なのか再び見ていきましょう。上記のコードは NSUserDefaults
で保存できるように、オブジェクトをデータに変換してくれます。実際にみてみましょう。
defaults = NSUserDefaults.standardUserDefaults
post = Post.new
post.message = "hello!"
post.id = 1000
post_as_data = NSKeyedArchiver.archivedDataWithRootObject(post)
defaults["saved_post"] = post_as_data
# あとで、次のようにこの post データをロードします
post_as_data = defaults["saved_post"]
post = NSKeyedUnarchiver.unarchiveObjectWithData(post_as_data)
encodeObject
と decodeObjectForKey
のこれらは長くてとても扱いにくいので、私たちの API モデルの構造を活用して、簡単に扱えるようにしましょう。
class User
...
def initWithCoder(decoder)
self.init
PROPERTIES.each { |prop|
value = decoder.decodeObjectForKey(prop.to_s)
self.send((prop.to_s + "=").to_s, value) if value
}
self
end
# NSUserDefaults へオブジェクトを保存するときに呼び出されます。
def encodeWithCoder(encoder)
PROPERTIES.each { |prop|
encoder.encodeObject(self.send(prop), forKey: prop.to_s)
}
end
素敵ですよね?Objective-C の最大の苦痛の一つはモデルに新しい属性を追加することが簡単ではないことです。Ruby コードの魔術は私たちのコードを変更することを楽にしてくれます。
今、私たちは 本当に 堅牢で柔軟なモデルを持っています。それでは、変更したものを画面上に配置してみましょう。
Key-Value Observing Example
キー値監視(Key Value Observing、KVO) は本当にすごくクールで、大幅に時間を節約できるでしょう。
RubyMotion では、ほかのオブジェクトの任意のプロパティを監視することができます。それでは、ユーザの名前を監視しているとしましょう。ユーザの "#name" プロパティの値が変更されるとき、コールバックによって自動的に新しい値を取得します。独自の構造や通知を書く必要はありません。
これの実用的な例は、UILabel
のようなオブジェクトのプロパティが常に @user.name
を示すように関連付けられている view があげられます。name を変更するとき、ラベルは自動的に更新されモデルと同期します。今からそのようなものを実装していきます。
新しい RubyMotion プロジェクトを motion create KeyValueFun
のように作成します。あと、User
モデルで遊ぶために ./app/user.rb
というファイルも作成します。
もう一つだけ必要なことがあります。BubbleWrap を使うことです。
BubbleWrap は iOS SDK を Ruby らしく記述できるような wrapper を集めたものです。Apple の API の多くは、コールバックするオブジェクト上であらかじめ決められたメソッドを呼び出す仕組みを使っています。これは Objective-C では好ましいものですが、無名関数が多用される Ruby では望まれません。そこで、BubbleWrap の wrapper の多くは、そのようなコールバックメソッドが単にラムダやブロックとして動作します。キー値監視を本当にシンプルにしてくれる wrapper を使用します。
サードパーティーのライブラリを RubyMotion にどのようにインストールするのでしょう?レポジトリをクローンしてそれをプロジェクトへ追加するか、RubyGems を使うことができます。後者は、サードパーティーのコードのインストールとメンテナンスがとても簡単になるので、新しい RubyMotion ライブラリを gem としてインストールすることができないかチェックすることをおすすめします(もしそうでなければ、fork して直してあげよう!)。
Terminal で gem install bubble-wrap
を実行します。ほかの gem のように、BubbleWrap がみなさんのマシンにインストールされます。しかし、RubyMotion 以外のプロジェクトで require "bubble-wrap"
とした場合にはエラーとなります。
次のように、Rakefile
で require "bubble-wrap"
を追加します。
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
require 'bubble-wrap'
...
さぁ、コードを書いてみましょう。user.rb
で、上記の概要に沿った堅牢で API を構築しやすい User
クラスか、より簡易的なバージョンを使うことができます。
class User
attr_accessor :id
attr_accessor :name
attr_accessor :email
end
AppDelegate
では、User
の id
、 name
と email
の各フィールドのラベルを作ります。そのあと、オブジェクトのこれら属性の監視を開始し、それに従って各ラベルを更新します。簡単そうですか?さぁ、それを見ていきましょう。
class AppDelegate
include BW::KVO
attr_accessor :user
...
BubbleWrap のナイスな KVO wrapper を使用するために、メソッドを include
する必要があります。この操作は、プロパティの監視をしたい任意のオブジェクトに対して行う必要があります。デバッグを楽にするために、#user
という属性も追加します。view は以下のようになります。
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.applicationFrame)
@window.makeKeyAndVisible
@name_label = UILabel.alloc.initWithFrame([[0, 0], [100, 30]])
@window.addSubview(@name_label)
@email_label = UILabel.alloc.initWithFrame([[0, @name_label.frame.size.height + 10], @name_label.frame.size])
@window.addSubview(@email_label)
@id_label = UILabel.alloc.initWithFrame([[0, @email_label.frame.origin.y + @email_label.frame.size.height + 10], @name_label.frame.size])
@window.addSubview(@id_label)
...
@window
にすべてのラベルを追加し、縦に並べています。みなさんはもっときれいな体裁にできますが、ここでは簡単にしておきます。
最後にメインディッシュの observe
です。
...
self.user = User.new
["name", "id", "email"].each { |prop|
observe(self.user, prop) do |old_value, new_value|
instance_variable_get("@#{prop}_label").text = new_value
end
}
true
end
end
KVO の observe
メソッドは、observe(#<object to be observed>, "the property") do ....
という形式です。#each
メソッドの重複を取り除くためにここで Ruby の素晴らしいトリックを使います。もしプロパティがひとつだけでしたら次のようにすることができます。
observe(self.user, "name") do |old_value, new_value|
instance_variable_get("@name_label").text = new_value
end
最高でしょう? あと、次のことは覚えておいてください。監視実行時の self.user
で起きていることのみを監視するようにしているので、self.user
に再度オブジェクトを割り当てると監視動作は停止します。self.user = some_other_user
とすると停止します。
どのようにテストしたら良いでしょう?rake
を実行してデバッグコンソールへ向かいましょう。私たちが正しく掘り下げていれば、User
オブジェクトを取得することができ、それで遊ぶことができるはずです。
$ rake
...
(main)> delegate = UIApplication.sharedApplication.delegate
=> #<AppDelegate .... >
(main)> user = delegate.user
=> #<NSKVONotifying_User>
(main)> user.email = "test@mail.com"
=> "test@mail.com"
(main)> user.name = "clay"
=> "clay"
(main)> user.id = "9000"
=> "9000"
次のような結果となるはずです。
Wrapping Up
ちょっとだけ iOS の魅力的な部分で遊びましたが、確固たるモデルの構造と KVO はユーザエクスペリエンスと同様に重要です。次のことを、ここで学びました。
- モデルは普通の Ruby オブジェクトです。
attr_[accessor/reader/writer]
メソッドを使ってプロパティを追加できます。 - 属性の定義に
PROPERTIES
という定数を使いました。initialize
メソッドでハッシュから読み込むのが本当に簡単になります。 NSUserDefaults
はプリミティブな永続的ストアです。モデルを保存するためにNSKeyedArchiver
とNSKeyedUnarchiver
を使います。initWithCoder:
とencodeWithCoder:
をみなさんのクラスに実装する必要があります。- キー値監視(Key Value Observing、KVO) はみなさんのオブジェクトに、別のオブジェクトのプロパティの変更の通知を受け取ることができるようにします。
- 簡単にキー値監視を行うために
include BW::KVO
とobserve
を使いました。