Easy CoreData integration for querying and persistence provided for RubyMotion projects for iOS and Mac OS X.
Forked from the mattgreen/nitron gem, this provides an intuitive way to query and persist data in CoreData, while letting you use the powerful Xcode data modeler and versioning tools for schema definitions. Even includes automatatic lightweight migrations!
Recommended installation is to use Bundler.
Add the following to your project's
Gemfile to work with bundler.
Install with bundler:
Xcode Data Modeler
MotionDataWrapper does not provide schema or migration classes wrapping any of the provided tools from Apple. Instead, we load in the data model file created in Xcode and let CoreData map out the entities, relationships, validations, and migrations. This allows you to have the full toolset provided by CoreData, without being limited by our implementation.
What we do provide is an easy way to integrate with CoreData for the most common tasks, such as persistence and querying the object graph.
Follow these instructions to get started on your project with MotionDataWrapper:
- Add the gem to your Gemfile if not done already
- Create a new Xcode project in the root of your RubyMotion project, add the "name.xcodeproj" file to your .gitignore file as it is not needed
- Inside Xcode, create a new "Data Model" file and save that in your "resources" folder for RubyMotion to automatically compile when running
- From the "Editor" menu, select "Add Model Version..." and type whatever name you want.
- Select this new model version and in the attributes inspector, provide a "Core Data Model" identifier, ie "1.0"
- Set this version as the current version of your CoreData object model, with these instructions.
- Setup your entities, relationships, validations and migrations in Xcode, which is outside the scope of this tutorial.
- Include the
MotionDataWrapper::Delegatemodule into your application delegate. For available options to customize this behavior for injecting the needed methods to setup CoreData in your iOS/Mac project, see this file.
- Define your model classes in Ruby code to correspond to each entity defined in Xcode.
- Utilize your new model classes as documented farther down.
# app/app_delegate.rb class AppDelegate include MotionDataWrapper::Delegate end # app/models/post.rb class Post < MotionDataWrapper::Model end
ActiveRecord Style Syntax
MotionDataWrapper offers lots of your favorite ActiveRecord features for querying and persisting objects, including the following methods.
Most of the familiar Rails AR query methods are available, even dynamic finders, including the following:
# Querying Tasks Task.all # Array of tasks Task.pluck(:assignee_id) # returns array of non-distinct values Task.uniq.pluck(:assignee_id) # now the array of id values is distinct Task.first # First task or nil Task.last # Last task or nil Task.limit(1) # returns one task Task.offset(5).limit(1) # grab the 6th task, as an array with one item in it Task.where("title contains[cd] ?", "some") # grab all tasks with the title containing "some", case insensitive Task.where("title contains[cd] ?", "some").count # db call to count the objects matching the conditions Task.count # number of tasks in the system Task.order(:title, ascending: false) # Tasks order in reverse alphabetical order on title attribute # Testing for existance Task.where("title contains[cd] ?", "some").exists? Task.where("title contains[cd] ?", "some").empty? # Overriding existing query scope = Task.where("status = ?", :open) scope.except(:where).where("status = ?", :closed) # realized I really wanted closed items scope.order(...).except(:order) scope.limit(...).except(:limit) # Daisy Chaining Task.where(...).order(...).where(...).offset(10).limit(5).count # Yep, this works! Task.where(...).order(...).all # array of the results Task.where(...).first! # raises MotionDataWrapper::RecordNotFound exception # Dynamic Finders Task.find_by_status :open # returns the first task with a status of open, or nil Task.find_all_by_status :open # returns array containing Tasks matching that status # Search in a specific managed object context Task.with_context(bg_ctx).where(...) # searches using a specific context, default is App.delegate.managedObjectContext
MotionDataWrapper supports the normal convention of
#create! from ActiveRecord. However, always be aware that in CoreData there is no way to save just one instance in the managed object context to the persistent store, it's all or nothing. As such, calling any of these persistence methods will cause all unstored instances to be persisted to the context.
MotionDataWrapper includes support for model validations, defined in your Xcode data model file. On the above methods, CoreData will automatically run the validations anyway and MotionDataWrapper provides more helpful user-friendly error messages to display to the user, as well as the familiar
Supported persistence methods:
# Creating tasks Task.create assignee_id: 1, title: "some title" # runs validations, saves object into the default context if validations pass Task.create! # MotionDataWrapper::RecordNotSaved thrown if validations fail Task.new # creates a new Task object, outside of a NSManagedObjectContext, optionally takes attributes task = Task.new task.save # will save, true if successful, false if failed task.save! # will throw MotionDataWrapper::RecordNotSaved if failed, contains errors object for validation messages
MotionDataWrapper adds support for callbacks in the object lifecycle. Note that unlike ActiveRecord in Rails, these are not class methods that accept symbols or procs, but rather an instance method that the framework will call if defined. None of the methods take arguments, and the return values do not alter the lifecycle in any way (open a PR if you want to add that!)
There are the common ones you would expect, detailed in the following example:
class Task MotionDataWrapper::Model def before_create puts "called once in object lifecycle" end def after_create puts "called after the context was saved, if the object was inserted" end def before_update puts "called every time the object is dirty and the context is saved" end def after_update puts "called after the context is saved & if the object was dirty" end def before_destroy puts "called before the object is destroyed" end def after_destroy puts "object is removed from context, is frozen" end end
Relationships and Persistence
You define the relationships between models (one-to-one, many-to-many, optional, etc) in the Xcode CoreData modeler, and there is plenty of documentation online for using Apple's tool so there are plenty of options for reading more online.
You create and name the relationship, with validations etc in the modeler, and those are exposed on the model just like in ActiveRecord for Rails. For instance:
# one to many matt = Person.new matt.addresses << Address.new(street: "123 Lane") matt.addresses << Address.new(country: "USA") matt.save! # many to one Address.first.person # => matt sax = Person.new sax.save! # raises error if validation required at least one Address for instance
The pain in the ass comes when you are dealing with different CoreData NSManagedObjectContext objects. When you use
new in MDW, you have not yet inserted the newly created object into a context, and therefore it is not persisted to disk (even when another object calls
The core principal is that calling
#save saves the entire context, not just the one record. Always.
So doing something like this will fail, as
addr is not in the same context as
matt is immediately inserted into the context and attempted to be saved.:
addr = Address.new street: "123" matt = Person.create address: add
This would work however:
addr = Address.new street: "123" ctx = App.delegate.managedObjectContext ctx.insertObject(addr) # inserted into context, but not yet persisted matt = Person.create address: add
At least this way, both objects are in the same context, so the required relationships can be set, and then the context will save successfully.