RubyMotion で CoreData を使う

現在、RubyMotion の評価中。無料で試用できないのはつらいけど、30日間は返金に応じてくれるみたいなので、ポチってみた。

関心ごとの筆頭は、RubyMotion で CoreData が使えるかどうか。ネットで見つけた記事はどれも Web API 呼び出すやつばかりだったんで。

結論を先に書くと、RubyMotion でも CoreData は使える。 RubyMotionSamples というサンプル集に CoreData と CoreLocation を使ったサンプルがあった。

上記のサンプルは分かりやすかった。RubyMotionSamples は一通り目を通すべき。 でも実際に手を動かさないと身に付かないので、CoreData をベタ書きした、さらにシンプルなサンプルを書いてみた。

まず新しいプロジェクトを作成。

motion create CoreDataSample

Rakefile 内で、使用するフレームワークに CoreData を追加する。

# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/ios'

begin
  require 'bundler'
  Bundler.require
rescue LoadError
end

Motion::Project::App.setup do |app|
  app.name = 'CoreDataSample'
  app.frameworks += ['CoreData']
end

あとはひたすらコードを書く。Xcode では GUI で作成できたエンティティの定義も、RubyMotion だとコードで書く。NSManagedObjectContext を初期化したり、データの取得・追加・削除のやり方は Objective-C と変わらない。

今回は app_delegate.rb にすべて書いた。

# coding: utf-8

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    nav = UINavigationController.alloc.initWithRootViewController(MasterController.alloc.init)
    nav.wantsFullScreenLayout = true
    nav.toolbarHidden = true
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = nav
    @window.makeKeyAndVisible
    true
  end
end

class Entry < NSManagedObject
  # CoreData のエンティティを定義。
  # Xcode では GUI で定義できたけど、RubyMotion ではコードを書かないといけない。
  def self.entity
    @entity ||= begin
      entity = NSEntityDescription.alloc.init
      entity.name = 'Entry'
      entity.managedObjectClassName = 'Entry'
      entity.properties = 
        ['creation_date', NSDateAttributeType].each_slice(2).map do |name, type|
            property = NSAttributeDescription.alloc.init
            property.name = name
            property.attributeType = type
            property.optional = false
            property
          end
      entity
    end
  end 
end

class MasterController < UITableViewController
  def viewDidLoad
    view.dataSource = self
    view.delegate = self
  end

  # CoreData 関連のクラスを初期化
  def managed_object_context
    @context ||= begin
      model = NSManagedObjectModel.alloc.init
      model.entities = [Entry.entity]

      store = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(model)
      store_url = NSURL.fileURLWithPath(File.join(NSHomeDirectory(), 'Documents', 'Guestbook.sqlite'))
      error_ptr = Pointer.new(:object)
      unless store.addPersistentStoreWithType(NSSQLiteStoreType, configuration:nil, URL:store_url, options:nil, error:error_ptr)
        raise "Can't add persistent SQLite store: #{error_ptr[0].description}"
      end
  
      context = NSManagedObjectContext.alloc.init
      context.persistentStoreCoordinator = store
      context
    end
  end
 
  def save
    error_ptr = Pointer.new(:object)
    unless managed_object_context.save(error_ptr)
      raise "Error when saving the model: #{error_ptr[0].description}"
    end
    @entries = nil
  end

  def viewWillAppear(animated)
    navigationItem.title = 'Guestbook'
    navigationItem.leftBarButtonItem = editButtonItem
    navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemAdd, target:self, action:'addEntry')
  end

  def addEntry
    # 追加
    entry = NSEntityDescription.insertNewObjectForEntityForName('Entry', inManagedObjectContext:managed_object_context)
    entry.creation_date = NSDate.date
    save
    
    view.reloadData
  end
 
  # SQLite に保存したデータを取得。 
  def entries
    @entries ||= begin
      request = NSFetchRequest.alloc.init
      request.entity = NSEntityDescription.entityForName('Entry', inManagedObjectContext:managed_object_context)
      request.sortDescriptors = [NSSortDescriptor.alloc.initWithKey('creation_date', ascending:false)] 

      error_ptr = Pointer.new(:object)
      data = managed_object_context.executeFetchRequest(request, error:error_ptr)
      if data == nil
        raise "Error when fetching data: #{error_ptr[0].description}"
      end
      data
    end
  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]
    
    # 削除
    managed_object_context.deleteObject(entry)
    save

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

rake でシミュレーターを起動すると CoreData を使ったサンプルアプリが動く。

RubyMotion でも CoreData は使えたけど、結構なコード量になった。Vim でこれだけのコードを書くのはシンドイ。