MotionDataWrapper

はじめに

RubyMotion で CoreData を使うなら MotionData を導入した方がいい」って勧めておいてなんだけど、 その後『MotionDataWrapper』を知って速攻で移行してしまった。

MotionDataWrapper の特徴

MotionData は DataMapper ライクに CoreData のエンティティを定義したのに対し、 MotionDataWrapper では Xcode Data Modeler で作成したエンティティ定義を使うことが前提になっている。

Xcode Data Modeler で作成したエンティティ定義を使うと割り切っているため、 MotionDataWrapper の実装はコンパクト。 CoreData の初期化と ActiveRecord ライクな Finder が機能のほとんどを占める。 非常に見通しがいいので、すべてのコードを読むのは容易だった。

Xcode Data Modeler を使うので、スキーマのバージョ二ングができる。 試してないけど、マイグレーションもできそう。 これらは MotionData にはまだ無い機能だ。

MotionDataWrapper のインストール

Gemfile に

gem 'motion_data_wrapper'

を記述して、

bundle

を実行すればインストールできる。

MotionDataWrapper を使ったサンプル

上記の記事で作成したサンプルを、MotionDataWrapper を使って書き変えてみる。

エンティティの定義

エンティティの定義は Xcode を使う。

まず Xcode で空のプロジェクトを新規作成。

f:id:griefworker:20131106230212p:plain

xcodeproj ファイルの保存先は 、app ディレクトリと resources ディレクトリ以外ならどこでもいい。

次に Data Model ファイルを作成。

f:id:griefworker:20131106230227p:plain

保存場所は resources ディレクトリを選択する。

スキーマのバージョンを指定して

f:id:griefworker:20131106230901p:plain

エンティティを定義したら終了。

f:id:griefworker:20131106230915p:plain

MotionDataWrapper を使ってコードを記述

Xcode Data Modeler で定義したエンティティに対応するモデルクラスを、MotionDataWrapper::Model を継承して作成する。

class Entry < MotionDataWrapper::Model
end

AppDelegate で MotionDataWrapper::Delegate をインクルードして、CoreData 関連オブジェクトを(ManagedObjectContext など)を取得するメソッドを追加。

class AppDelegate
  # CoreData の初期化は Delegate が中でやってくれる
  include MotionDataWrapper::Delegate

  # ...省略...
end

データの CRUD 操作に関しては、MotionDataWrapper::Model が ActiveRecord ライクなメソッドを提供してくれる。

MotionDataWrapper を使ったサンプルコードの全体は次の通り。

# coding: utf-8

class AppDelegate
  # CoreData の初期化は Delegate が中でやってくれる
  include MotionDataWrapper::Delegate

  def application(application, didFinishLaunchingWithOptions:launchOptions)
    nav = UINavigationController.alloc.initWithRootViewController(
      MasterController.alloc.init
    )

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = nav
    @window.makeKeyAndVisible

    true
  end
end

# モデルの定義はこれだけ
class Entry < MotionDataWrapper::Model
end

class MasterController < UITableViewController
  def viewDidLoad
    super
    navigationItem.title = 'Guestbook'
    navigationItem.leftBarButtonItem = editButtonItem
    navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem(
      UIBarButtonSystemItemAdd,
      target:self,
      action:'addEntry'
    )
  end

  def addEntry
    # 追加
    Entry.create(creation_date: NSDate.date)
    @entries = nil
    
    view.reloadData
  end
 
  # SQLite に保存したデータを取得。 
  def entries
    @entries ||= Entry.all
  end

  def tableView(tableView, numberOfRowsInSection:section)
    entries.size
  end

  CellID = 'CellIdentifier'
  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    cell = tableView.dequeueReusableCellWithIdentifier(CellID) ||
      UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:CellID)
    
    entry = entries[indexPath.row]
    @date_formatter ||= NSDateFormatter.alloc.init.tap do |df|
      df.timeStyle = NSDateFormatterMediumStyle
      df.dateStyle = NSDateFormatterMediumStyle
    end
    cell.textLabel.text = @date_formatter.stringFromDate(entry.creation_date)

    cell
  end

  def tableView(tableView, editingStyleForRowAtIndexPath:indexPath)
    UITableViewCellEditingStyleDelete
  end

  def tableView(tableView, commitEditingStyle:editingStyle, forRowAtIndexPath:indexPath)
    # 削除
    entry = entries[indexPath.row]
    entry.destroy
    @entries = nil

    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation:UITableViewRowAnimationFade)
  end
end

scope を定義できないのが不満だけど、Pull Request を覗いてみたら実装中だったので、 そのうちマージされそう。

まとめ

作り捨てのアプリなら MotionData でもいい。 長く保守するつもりのアプリなら、スキーマの変更がいつか必要になるから、現時点では MotionDataWrapper を使った方がいいと思う。