RubyMotion で CoreData を使うとき Xcode で作成した Data Model をラッパー gem なしで読み込む

先日 RubyMotion で CoreData を使うために試した MotionDataWrapper、 ソースコードを見たらシンプルな実装で難しいことやっていなかった。

Xcode の Data Modeler で作成したエンティティ定義を読み込むけど、 それは CoreData.framework が提供している機能を呼び出しているだけ。

これなら MotionDataWrapper 使わず、RubyMotion で直に書けそう。 実際に試してみた。

まず Xcode で Empty プロジェクトを作成。

f:id:griefworker:20131106230212p:plain

次に Data Modelファイルを作成。ファイルの保存先は resources 直下で、 ファイル名はとりあえずデフォルトの Model のままにしておく。

f:id:griefworker:20131106230227p:plain

エンティティを定義したら Xcode での作業は終了。

f:id:griefworker:20131216194751p:plain

RubyMotion で Data Model ファイルを使うサンプルを作成。

# coding: utf-8

class Task < NSManagedObject
end

class TasksViewController < UITableViewController
  def viewDidLoad
    super
    self.navigationItem.title = "Tasks"
    self.navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem(
      UIBarButtonSystemItemAdd,
      target: self,
      action: "add_task"
    )
    self.navigationItem.leftBarButtonItem = self.editButtonItem
  end

  def tableView(tableView, numberOfRowsInSection:section)
    self.tasks.size
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    task = self.tasks[indexPath.row]
    cell = tableView.dequeueReusableCellWithIdentifier("Cell")
    if cell == nil
      cell = UITableViewCell.alloc.initWithStyle(
        UITableViewCellStyleDefault,
        reuseIdentifier:"Cell"
      )
    end
    cell.textLabel.text = task.title
    cell
  end

  def tableView(tableView, commitEditingStyle:editingStyle, forRowAtIndexPath:indexPath)
    if editingStyle == UITableViewCellEditingStyleDelete
      task = self.tasks[indexPath.row]
      delete_task(task)
      tableView.deleteRowsAtIndexPaths(
        [indexPath],
        withRowAnimation:UITableViewRowAnimationFade
      )
    end
  end

  def tasks
    @tasks ||= begin
      request = NSFetchRequest.fetchRequestWithEntityName("Task")
      sortDescriptor = NSSortDescriptor.sortDescriptorWithKey("createdAt", ascending:true)
      request.sortDescriptors = [sortDescriptor]

      error = Pointer.new(:object)
      results = AppDelegate.sharedDelegate.managedObjectContext.executeFetchRequest(request, error:error)
      results
    end
  end

  def add_task
    task = NSEntityDescription.insertNewObjectForEntityForName(
      "Task",
      inManagedObjectContext:AppDelegate.sharedDelegate.managedObjectContext
    )
    task.title = "test"
    task.createdAt = NSDate.date

    AppDelegate.sharedDelegate.saveContext
    @tasks = nil
    self.tableView.reloadData
  end

  def delete_task(task)
    AppDelegate.sharedDelegate.managedObjectContext.deleteObject(task)
    AppDelegate.sharedDelegate.saveContext
    @tasks = nil
  end
end

class AppDelegate
  def self.sharedDelegate
    UIApplication.sharedApplication.delegate
  end

  def application(application, didFinishLaunchingWithOptions:launchOptions)
    navi = UINavigationController.alloc.initWithRootViewController(
      TasksViewController.alloc.init
    )
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = navi
    @window.makeKeyAndVisible
    true
  end

  def applicationWillTerminate(application)
    self.saveContext
  end

  def saveContext
    error = Pointer.new(:object)
    managedObjectContext = self.managedObjectContext
    if managedObjectContext != nil
      if managedObjectContext.hasChanges and !managedObjectContext.save(error)
        NSLog("Unresolved error #{error[0]}, #{error[0].userInfo}")
        abort()
      end
    end
  end

  def managedObjectContext
    @managedObjectContext ||= begin
      coordinator = self.persistentStoreCoordinator
      if coordinator != nil
        managedObjectContext = NSManagedObjectContext.alloc.init
        managedObjectContext.setPersistentStoreCoordinator(coordinator)
        managedObjectContext
      else
        nil
      end
    end
  end

  def managedObjectModel
    @managedObjectModel ||= begin
      modelURL = NSBundle.mainBundle.URLForResource("Model", withExtension:"momd")
      NSManagedObjectModel.alloc.initWithContentsOfURL(modelURL)
    end
  end

  def persistentStoreCoordinator
    @persistentStoreCoordinator ||= begin
      storeURL = self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataSample2.sqlite")
      error = Pointer.new(:object)
      persistentStoreCoordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(self.managedObjectModel)
      unless persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration:nil, URL:storeURL, options:nil, error:error)
        NSLog("Unresolved error #{error[0]}, #{error[0].userInfo}")
        abort()
      end
      persistentStoreCoordinator
    end
  end

  def applicationDocumentsDirectory
    NSFileManager.defaultManager.URLsForDirectory(
      NSDocumentDirectory,
      inDomains:NSUserDomainMask
    ).last
  end
end

CoreData の初期化部分は、 CoreData を使う iOS アプリのプロジェクトを Xcode で新規作成したとき生成されるソースコードと、 やってることはほぼ同じ。

RubyMotion で直に書けはしたけど、記述量が多くて毎回これ書こうとは思えない。 CoreData 部分をモジュール化して再利用するか、やっぱりライブラリ使うだろうな。