Fork me on GitHub

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 のインスタンスが渡され、与えられたキーでプリミティブなオブジェクトをエンコードします。NSKeyedArchiverNSKeyedUnarchiver はアーカイブされたオブジェクトを保存形式に変換したり、逆に保存形式からアーカイブされたオブジェクトに変換するためにさきほどのメソッドを使用します。

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)

encodeObjectdecodeObjectForKey のこれらは長くてとても扱いにくいので、私たちの 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" とした場合にはエラーとなります。

次のように、Rakefilerequire "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 では、Useridnameemail の各フィールドのラベルを作ります。そのあと、オブジェクトのこれら属性の監視を開始し、それに従って各ラベルを更新します。簡単そうですか?さぁ、それを見ていきましょう。

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"

次のような結果となるはずです。

KVO data

Wrapping Up

ちょっとだけ iOS の魅力的な部分で遊びましたが、確固たるモデルの構造と KVO はユーザエクスペリエンスと同様に重要です。次のことを、ここで学びました。

  • モデルは普通の Ruby オブジェクトです。attr_[accessor/reader/writer] メソッドを使ってプロパティを追加できます。
  • 属性の定義に PROPERTIES という定数を使いました。initialize メソッドでハッシュから読み込むのが本当に簡単になります。
  • NSUserDefaults はプリミティブな永続的ストアです。モデルを保存するために NSKeyedArchiverNSKeyedUnarchiver を使います。initWithCoder:encodeWithCoder: をみなさんのクラスに実装する必要があります。
  • キー値監視(Key Value Observing、KVO) はみなさんのオブジェクトに、別のオブジェクトのプロパティの変更の通知を受け取ることができるようにします。
  • 簡単にキー値監視を行うために include BW::KVOobserve を使いました。

RubyMotion でどのようにテストを行うかを見るためにフォースを使おう!


Like it? Spread the word