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



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

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

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

さてさて、今回の章ではモデルを扱います。

やっと出てきたモデル...。待ってたよ〜。

RubyMotionでは、モデルのために2つ大きなコンポーネントがあるそうです。

CoreDataとそれ以外の全て出そうです。(ざっくりだな〜...)

CoreDataはiOS向けのORMの一種で、ActiveRecordと同様のものだそうです。

ちなみに、ORMについての説明はこちら。

でも、ActiveRecordと同じものが使えるのはありがたいね〜。

チュートリアルでは、「CoreData以外の全て」を扱うそうです。

...。要するにCoreDataは自分で勉強しろってことね。。。ひどい;_;

※追記-CoreDataについてのブログ

Model-T

基本的なUserオブジェクトを作ってみます。そうですね、、、。ホームディレクトリにuser.rbファイルを作りましょう。そこに次のコードを書いてください。

$ cd
$ touch user.rb
class User
 attr_accessor :id
 attr_accessor :name
 attr_accessor :email
end

p @user = User.new
p @user.name = "Clay"
p @user.email = "clay@mail.com"

このコードを実行してみましょう。

$ ruby user.rb

ちゃんと動作していることがわかると思います。次は、Userをより便利に使えるように編集しましょう。Railsで定義する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)
p @user.name
$ ruby user.rb

うん、ちゃんと「Clay」が返ってきましたね。単に定数「PROPERTIES」を拡張することでより多くのプロパティを簡単に追加できるようになります。

ここは簡単なんで大丈夫かな?とりあえず、もう使わないuser.rbを削除して次行こう。

$ rm user.rb


NSCoding

なんか永続的なkey-valueストアとして「NSUserDefaults」というオブジェクトがあるそうです。通常、オブジェクトとして「NSUserDefaults.standardUserDefaults」のインスタンスを使うことができるそう。。。

ふぇ???なんかよくわからんくなってきたぞ。

とりあえず、Webと違ってサーバーにデータを保存しなくても、スマホ(アプリ)に保存できる機能があるってことね。

とりあえず、コードを見てみよう。

@defaults = NSUserDefaults.standardUserDefaults
@defaults["one"] = 1
# 別の場所で、あるいは、アプリを起動し直しても値を取得できます
@defaults["one"]
=> 1

NSUserDefaultsの値はアプリがインストールされているあいだは保存されているそう。なかなかすげーな!

値を消去する場合は、「NSUserDefaults.resetStandardUserDefaults」を実行することで全てのエントリーを消去できるそうです。

ただ、NSUserDefaultsには注意があるそうです。チュートリアルを引用。

ここで注意があります。NSUserDefaults には古いオブジェクトを格納することはできません。プリミティブな値(string, integer, hash など)の格納や生の data/byte 列 は使用することができます。User オブジェクトはプリミティブではないため、data メソッドを使う必要があります。

ごめんなさい、???って感じです。まぁ、やって行ったらわかるっしょ!!!

NSkyeedArchiverを使ってアーカイブ処理を行うことでモデルを配置できるそうです。アーカイブ処理は、圧縮処理ってことね。

NSKeyedArchiverクラスはオブジェクトを受付NSUserDefaultをで保存できるNSDataのインスタンスを作ります。アーカイブできるオブジェクトはNSCodingに準拠します。つまり、標準的なAPIをしようし自信をシリアライズ・デシリアライズする方法を定義する2つのメソッドを実装することを意味します。もし、モデルがこれらのメソッドを実装していない場合、アーカイブすることができません。

(;_;)何を言ってるの??とりあえず、次をみて行きますか。。。

NSCodingに準拠するための2つのメソッドはinitWithCoder(オブジェクトを読み込むために使われる)とencodeWithCoder:(オブジェクトを保存するために使われる)だそうです。

んー?取得と保存ができるのはわかったけど、更新と削除するメソッドはどこー?よくわからんから保留。

これら両方のメソッドはにはNSCoderのインスタンスが渡され、与えられたキーでプリミティブなオブジェクトをエンコードするそうです。

NSKeyedArchiverとNSKeyedUnarchiverはアーカイブされたオブジェクトを保存形式に変換したり、逆に保存形式からアーカイブされたオブジェクトに変換するために先ほどのメソッドを使用するそうです。

ふむふむ、これは何となくわかる。

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

NSUserDefaultで保存できるように、オブジェクトをデータに変換してくれているそうです。

実際にみてみましょう。

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とdecodeObjectForkyeのこれらは長くとても扱いにくいので、APIモデルの構造を活用して、簡単に扱えるようにするそうです。
まぁ、チュートリアルではidとmessageしかないので短くするメリットってないですけどね。
普通は、もっとたくさんあるので、覚えて置くといいってことだね!

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
 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
end

うん、ここまで書いたけど、理解度50%くらいかな〜。

とりあえず、あとで見返すとして今は保留!

Key-Value Observing Example

キー値監視(Key Value Observing、KVO)っていう便利なことができるらしい。

どういうことか、チュートリアルを引用。

RubyMotion では、ほかのオブジェクトの任意のプロパティを監視することができます。それでは、ユーザの名前を監視しているとしましょう。ユーザの "#name" プロパティの値が変更されるとき、コールバックによって自動的に新しい値を取得します。独自の構造や通知を書く必要はありません。

これはいいね!便利そうだし、利用イメージがわく!

とりあえず、ここで新しいプロジェクトを作成するみたい。

これまでの復習になるといいなー。

$ motion create KeyValueFun
$ cd KeyValueFun
$ touch app/user.rb

ついでに、user.rbも作っときました。

んで、ここでBubbleWrapを使うんだそうです!BubbleWrapってなんぞ?って感じなので、チュートリアルを引用します。

BubbleWrap は iOS SDK を Ruby らしく記述できるような wrapper を集めたものです。Apple の API の多くは、コールバックするオブジェクト上であらかじめ決められたメソッドを呼び出す仕組みを使っています。これは Objective-C では好ましいものですが、無名関数が多用される Ruby では望まれません。そこで、BubbleWrap の wrapper の多くは、そのようなコールバックメソッドが単にラムダやブロックとして動作します。キー値監視を本当にシンプルにしてくれる wrapper を使用します。

んで、これを使うにはgemでインストールすればいいらしいです。早速、ターミナルで「gem install bubble-wrap」を実行しましょう。

次にインストールしたbubble-wrapを使えるようにするために、requireを記述します。

$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
require 'bubble-wrap'
...

では、実際にコードを書いて行きましょう。user.rbで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
   }
   @window.rootViewController = UIViewController.alloc.initWithNibName(nil, bundle: nil)

   true
 end
end

KVOのobserveメソッドは、「observe(#<object to be observed>, "the property") do ....」という形式らしい。

注意しなきゃいけないのが、監視実行時のself.userで起きていることのみを監視するようにしているので、self.userにサイドオブジェクトを割り当てると監視動作は停止するそうです。要するに、self.user=xxxってすると停止する。

ちなみに、例のごとくrootがないとエラーが出るはずなんで、rootを追加してあげています。

さぁ、実行してみよ〜♪

うん、いい感じ〜♪んで、ちゃんと反映されてるかなぁ〜?

ぬお!?何も表示されてない...だと!!

なぜだ。。。

色々試した結果、単に黒背景に黒文字を表示していただけということが発覚しました。。。(;_;)

なので、「@window.rootViewController =...」の上の行に、「@window.backgroundColor = UIColor.whiteColor」を挿入して背景を白色にします。

改めて実行〜!!

よっしゃ!今度はうまくいったぜ(>v<)

ふ〜。モデルの章は長かった。。。

とりあえず、まとめ。

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

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

ようやく、MVCを一通りできたね〜。次はテストの方法を見て行きましょう!


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