Table of Contents
1. Introduction
Core Data
Core Data programming is hard. Apple warn as much in their document Core Data Core Competencies:
Important: Core Data is an advanced technology that is not required for creating simple applications.
That doesn’t sound so severe but it does contrast greatly with the rest of the documentation that seems to suggest that iOS/OSX programming is made easy by all the frameworks and widgets ready for us to customise and use in our applications.
Certainly, at each major release of iOS it seems that we have less to do everywhere, with new APIs being released constantly that do for us what we would have achieved before using the lower-level APIs. A recent example would be TextKit, released as part of iOS 7: having wrestled with typesetting in CoreText before I am certainly grateful that kerning, ligatures, hyphenation, line-breaking, justification and pretty much everything else that goes into making text look good is now considered to be a basic element of the platform. For those of us who are more software developer than designer this is most welcome indeed.
Would that we could say the same for Core Data. It is without doubt a solid and mature solution for persisting and syncing your app data, including between iOS, iCloud and OSX, but it hasn’t (yet) had quite the same user-friendliness refactoring that other parts of the platform have had.
TextKit, for example, which makes life so much easier when dealing with text, is simply built on top of CoreText. In the same way, we have 3rd-party libraries like MagicalRecord and Core Data Query that can make working with CoreData significantly easier. The problem with using these libraries is that you are going to hit problems that are unique to your app and then you will be largely at the mercy of the available documentation and perhaps the support of the developers of the library you are using to abstract away the less aesthetically-pleasing parts of CoreData. Given how new RubyMotion is, that documentation and community knowledge is perhaps going to be somewhat thin on the ground.
A more robust approach would be for us to first learn the raw Core Data API and delve into its inner workings. We will then be well placed to benefit from the use of 3rd party libraries, and perhaps even to extend them or write our own.
RubyMotion
RubyMotion has the potential to greatly increase the speed at which we create software for both iOS and OSX. Not only that but I also contend that the quality of the software you create may well increase too. Thanks to an ingenious implementation you not only have access to every last corner of the latest SDKs, you also have access to the same language constructs in Ruby that make Rails as powerful and pleasing to work with as it is1.
Core Data & RubyMotion
As you experiment with RubyMotion I think you will find that you are able to iterate more quickly than you could before, creating and customising your view controllers from the console and seeing the results live in the simulator.
You have perhaps already found though that when it comes to building an app that needs to store data that Core Data requires a prohibitive amount of boilerplate just to get the simplest of records saved.
So, you turn to one of the libraries that promises ActiveRecord-style ease of use, the ability to write model schemas just as you would in Rails perhaps, but what happens when you need to do something that isn’t documented? These 3rd-party libraries, which we will cover in this book, will accelerate your development for future apps but we’re also going to do our best to dive deep into Core Data itself and learn it in detail too–once you have completed this book a good way to advance your skills further might be to tackle some of the outstanding issues in their GitHub repositories.
Technical Notes and Bugs
This book has been written and tested with RubyMotion 2.32, with Xcode 5.1.1 installed on a MacBook running OSX 10.9.4.
Despite best efforts some bugs may slip into the text and code. There is nothing worse than code examples that don’t work–they can be a learning opportunity for the extremely positive-minded among us but to the rest of us they’re a source of wasted time. So, if you encounter major stumbling blocks or something just isn’t adding up please assume that it is my fault and not yours, either because there’s a mistake in the code or because I haven’t explained the concept well enough. In either case please drop me a line (stefan@haflidason.com) and I’ll puzzle it out with you.
Structure of this Book
Normally when you create an app using Core Data using Xcode you use the visual
data modeller to define your data model. This produces an .xcdatamodeld
bundle that contains not just your data model but each version of the data
model. With this bundle, Core Data can automatically load the latest version
and even perform the majority of schema migrations for you automatically.
Then, to set up the Core Data stack itself you might use the boilerplate code
which you can optionally have added to your project when you create it.
With RubyMotion though we are not only missing the visual data modeller but we have lost the option to add in that boilerplate Core Data stack-creation code too.
Thanks to some quality open-source libraries though (that we will review in this book) we need not worry though as in the following chapters we will learn how to:
- Model all of our data versions in a schema language much like that of Rails/ActiveRecord
- Set up a Core Data stack in 5 minutes and just two lines of code
- Access our entities as if they were (almost!) Rails models
- Evolve our data model using automatic migrations
- Set up an equivalent stack from scratch
- Set up and perform migrations manually, for when we need finer control.
- And finally, we will understand the limitations of all of the above.
All code from the book will be published here on GitHub. You will generally have a much easier time copying code from there rather than from the book. For the best learning experience however I would recommend typing out the examples to become familiar with each line as you progress. If you find a bug, please feel free to open an issue about it there.
The Principle of Minimal Magic
Those last three points are I think crucially important: there are lots of libraries out there that will help abstract away the complexity of frameworks like Core Data but when writing an app for the store you are bound to run into issues that are not (yet) documented online.
Our approach is going to be to make use of that magic on a regular basis and enjoy all the benefits to our productivity but to know how the underlying libraries work so that when we run into trouble we will be able to troubleshoot and move past it.
So without further ado, let’s dive into writing some RubyMotion-powered Core Data code!
2. Diving into Core Data
In this chapter we will set up a full Core Data stack purely in code, with no assistance from the graphical tools that Xcode provides. The aim here is to show that when you pull it all apart there is nothing terribly mysterious or complicated going on.
Later we will allow ourselves the use of some of the libraries out there to make our lives easier but first we’ll start with a good grounding in the fundamentals so that we will know how those libraries work and won’t get caught out later by ‘magic’ that we don’t understand.
We will now create the beginnings of a simple app that will use Core Data to store a list of tasks; we won’t implement the full app, rather we’ll just focus how to set up Core Data so that we can persist and retrieve our tasks.
Check your RubyMotion VersionThe code in this book has been tested primarily against RubyMotion version 2.32. If you are using a different version and you encounter an error, once you are satisfied that the error is not in your own code then you may want to try updating any included gems to the latest version as older versions of gems may be incompatible with more recent versions of RubyMotion. |
App Structure
To get started, we create a new RubyMotion app:
$
motion create CoreDataTasks
Create CoreDataTasks
Create CoreDataTasks/.gitignore
Create CoreDataTasks/app/app_delegate.rb
Create CoreDataTasks/Gemfile
Create CoreDataTasks/Rakefile
Create CoreDataTasks/resources/Default-568h@2x.png
Create CoreDataTasks/spec/main_spec.rb
And update our Rakefile
to load in the Core Data framework:
1
Motion
::
Project
::
App
.
setup
do
|
app
|
2
# Use `rake config' to see complete project settings.
3
app
.
name
=
'CoreDataTasks'
4
app
.
frameworks
+=
[
'CoreData'
]
5
end
That’s it! We now have a freshly-created app which will link in the CoreData
framework when the app is built.
Before we get into the code itself, let’s take a moment to familiarise ourselves with the components of the Core Data stack and how they work together to shuttle our records to and from the data store.
The Core Data Stack
A basic Core Data stack generally involves the following classes:
-
NSManagedObject
: Models of the data to be stored, each of which has an associated entity description (NSEntityDescription
) -
NSManagedObjectModel
: A container for our models, including any relationships between them. -
NSPersistentStore
: A place to store our records (a persistent store); in almost all cases you will want this to be an SQLite database (i.e. of typeNSSQLiteStoreType
) -
NSPersistentStoreCoordinator
: An object to coordinate access to this store -
NSManagedObjectContext
: An object which manages access to all of the above
This may look complicated, but in general your application code will only work
with the NSManagedObjectContext
instance that you create on application
launch, with the rest being essentially implementation details that your view
controllers and helper classes won’t be concerned with; they will simply use
the context to create and fetch records. In this way the Core Data stack looks
complex but once set up we will spend 99% of our time thinking about and
working with models and the context only.
Typically, the AppDelegate
is responsible for initialising the context (and so
the persistent store and data model) and passing it to the root view
controller. The root view controller would then pass it as appropriate to
child view controllers such as for adding or editing records. In this example
however we will provide access to the context from anywhere in the app via a
Singleton.
What is a Singleton?A Singleton is a class which only ever has one instance in a running application. The first time the Singleton class instance is requested the class will instantiate an instance of itself, store it locally and then return it. Subsequent calls will return this instance. This pattern is often used to provide access to a particular class (such as a data manager) throughout the application in a controlled manner while ensuring that there is only ever one instance of the class created during each run. |
To set up our Core Data stack we will step through the following process:
-
Setting up our entities. For our example tasks app we will define a
Task
entity with a description (what is to be done) a priority and a boolean (true/false value) to track whether it has been done or not. -
Create a managed object model containing one entity:
Task
. - Set up a persistent store coordinator for this managed object model.
- Create a physical data store and let the coordinator know about it; in doing so we will create an SQLite database file which will be stored in the application’s Documents directory).
- Create a managed object context and link it to the persistent storage coordinator.
Setting up our Entities
The first thing we do is describe the entity that we would like to store.
There is no fixed place that this code needs to go but we will put it in a new
file,
app/task.rb.
The Task class will be of type NSManagedObject
and will have one class
method that will return an NSEntityDescription
describing the fields it
contains:
app/task.rb
1
class
Task
<
NSManagedObject
2
def
self
.
entity
3
@entity
||=
begin
4
# A task has a description field, a numeric priority (where 5 will
5
# signify 'no priority' and will otherwise be set between 1 and 4,
6
# with 1 being the highest priority and 4 the lowest.
7
# A boolean field `completed` tracks whether the task has been
8
# completed or not
9
entity
=
NSEntityDescription
.
alloc
.
init
10
entity
.
name
=
'Task'
11
entity
.
managedObjectClassName
=
'Task'
12
entity
.
properties
=
13
[
'task_description'
,
NSStringAttributeType
,
14
'priority'
,
NSInteger32AttributeType
,
15
'completed'
,
NSBooleanAttributeType
]
\
16
.
each_slice
(
2
)
.
map
do
|
name
,
type
,
optional
|
17
property
=
NSAttributeDescription
.
alloc
.
init
18
property
.
name
=
name
19
property
.
attributeType
=
type
20
property
.
optional
=
false
21
property
22
end
23
24
# Return the entity
25
entity
26
end
27
end
28
end
You may be tempted to call the task description field just ‘description’,
but don’t! There is already a |
The class defines just one method, Task.entity
that returns an
NSEntityDescription
describing the entity. Note that by defining the method
as def self.entity
it is a class method that we call on the Task
class
directly rather than via a Task
instance.
When setting up your entities, the following field types are available:
|
Setting up the Stack
Now that we have our NSManagedObjectModel
we can move on to setting up the
rest of the Core Data Stack. Create a new file, app/tasks_store.rb
and type
in the following definition for a Singleton class called TasksStore
that we
will use to create a nice interface around the creation and fetching of Tasks:
app/tasks_store.rb
1
class
TasksStore
2
def
self
.
shared
3
Dispatch
.
once
{
@instance
||=
new
}
4
@instance
5
end
6
end
We will be able to access the sole instance of this class from anywhere in the
app as TasksStore.shared
. The first invocation will create the instance and
store it as @instance
, subsequent calls will simply return @instance
.
We’re calling new
to instantiate the class so we create an initialize
method for the class2. Here is the full
implementation,
we will then step through each part:
app/tasks_store.rb
1
class
TasksStore
2
def
self
.
shared
3
Dispatch
.
once
{
@instance
||=
new
}
4
@instance
5
end
6
7
def
initialize
8
# Initialise the Core Data stack
9
10
model
=
NSManagedObjectModel
.
alloc
.
init
11
model
.
entities
=
[
Task
.
entity
]
12
13
store
=
NSPersistentStoreCoordinator
\
14
.
alloc
.
initWithManagedObjectModel
(
model
)
15
store_path
=
File
.
join
(
NSHomeDirectory
(),
16
'Documents'
,
17
'CoreDataTasks.sqlite'
)
18
store_url
=
NSURL
.
fileURLWithPath
(
store_path
)
19
20
puts
"[INFO] Database file path:
#{
store_path
}
"
21
22
error_ptr
=
Pointer
.
new
(
:object
)
23
24
unless
store
.
addPersistentStoreWithType
(
NSSQLiteStoreType
,
25
configuration
:
nil
,
26
URL
:
store_url
,
27
options
:
nil
,
28
error
:
error_ptr
)
29
raise
"[ERROR] Failed to create persistent store: "
+
30
error_ptr
[
0
].
description
31
end
32
33
context
=
NSManagedObjectContext
.
alloc
.
init
34
context
.
persistentStoreCoordinator
=
store
35
36
# Store the context as an instance variable of TasksStore
37
@context
=
context
38
end
39
end
Creating the Managed Object Model
1
model
=
NSManagedObjectModel
.
alloc
.
init
2
model
.
entities
=
[
Task
.
entity
]
For this simple tasks app our object model only contains a single entity,
provided by the class method Task.entity
which we implemented earlier.
Creating the Persistent Store
1
store
=
NSPersistentStoreCoordinator
.
alloc
\
2
.
initWithManagedObjectModel
(
model
)
3
4
store_path
=
File
.
join
(
NSHomeDirectory
(),
5
'Documents'
,
6
'CoreDataTasks.sqlite'
)
7
store_url
=
NSURL
.
fileURLWithPath
(
store_path
)
8
9
puts
"[INFO] Database file path:
#{
store_path
}
"
10
11
error_ptr
=
Pointer
.
new
(
:object
)
12
13
unless
store
.
addPersistentStoreWithType
(
NSSQLiteStoreType
,
14
configuration
:
nil
,
15
URL
:
store_url
,
16
options
:
nil
,
17
error
:
error_ptr
)
18
raise
"[ERROR] Failed to create persistent store: "
+
19
error_ptr
[
0
].
description
20
end
Breaking it down
- We instantiate an
NSPersistentStoreCoordinator
with the managed object model we’ve just created. - We then set up the persistent store that will store the records: we name the SQLite database file CoreDataTasks.sqlite and place it in the Documents directory where all such local content must live within an app.
- We print the path out to make it easy to get at the database file (when running the app in the simulator) in case we want to access it directly, or delete it and start afresh.
- Finally we pass the persistent store to the coordinator.
What’s Pointer.new(:object)?A legacy of C, error codes and messages for many methods are stored in an
NSError object that we pass in to the function. When you have need of such
a pointer you can use the |
Creating the Managed Object Context
1
context
=
NSManagedObjectContext
.
alloc
.
init
2
context
.
persistentStoreCoordinator
=
store
3
4
# Store the context as an instance variable of TasksStore
5
@context
=
context
Finally, we create the context and link it to the persistent storage
coordinator. We store the context in an instance variable @context
for use
in the rest of the class.
A Note on the Managed Object ContextThink of the context as a scratch pad where you manage the objects that you are working with in your app. You will use the context to manage object creation, editing, deletion, undo and redo all without necessarily touching the store–you persist all of these changes when it makes sense for your app, e.g. after the user taps a ‘Done’ button after editing, or when the app is sent to the background (the user presses the home button). To persist the records you are working with you will simply ask the context to save itself and, barring any errors, it will handle all of the record creation, deletion, editing, reverts etc. for you. |
Creating, Reading, Updating and Deleting Records
At this point we have a full Core Data stack set up, ready for us to persist
and retrieve Task
objects via the managed object context.
Let’s start by adding a method create_task
to our TasksStore
class that
will let us create a new task:
1
def
create_task
(
task_description
,
2
withPriority
:
priority
,
3
andCompleted
:
completed
)
4
task
=
NSEntityDescription
\
5
.
insertNewObjectForEntityForName
(
'Task'
,
6
inManagedObjectContext
:
@context
)
7
task
.
task_description
=
task_description
8
task
.
priority
=
priority
9
task
.
completed
=
completed
10
task
11
end
12
13
def
create_task
(
task_description
)
14
create_task
(
task_description
,
withPriority
:
5
,
andCompleted
:
false
)
15
end
Method Overloading in RubyMotionNote that we have defined the Rather, method definitions with named parameters will look more like
Objective-C methods, with named parameters such as If you have a background primarily in Ruby and not in Objective-C then it
may now look to you like we have two methods called When we get to defining the |
If you build and run the app now you will now be able to create tasks:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
(
main)
> bank
=
tasks_store.create_task(
"Go to the bank"
)
4
=
> #<Task_Task_:0x8c9ce50>
The task has not been saved yet though, it has only been inserted into the
context in memory but the context has not yet been persisted to the store. So
now let’s create a helper method save_context
that will, when called, save
any changes we have made to the context, back to the persistent store (our
SQLite database):
1
def
save_context
2
error_ptr
=
Pointer
.
new
(
:object
)
3
4
unless
@context
.
save
(
error_ptr
)
5
raise
"[ERROR] Error saving the context:
#{
error_ptr
[
0
].
description
}
"
6
end
7
8
true
9
end
Now if we build and run the app we will be able to persist our tasks to the
store but as yet we don’t have a way to retrieve them, so let’s add a method
get_tasks
that will retrieve all tasks from the store:
1
def
get_tasks
2
request
=
NSFetchRequest
.
alloc
.
init
3
request
.
entity
=
NSEntityDescription
\
4
.
entityForName
(
'Task'
,
5
inManagedObjectContext
:
@context
)
6
7
error_ptr
=
Pointer
.
new
(
:object
)
8
9
data
=
@context
.
executeFetchRequest
(
request
,
error
:
error_ptr
)
10
11
if
data
==
nil
12
raise
"[ERROR] Error fetching taskss:
#{
error_ptr
[
0
].
description
}
"
13
end
14
15
data
16
end
Retrieval is a little more involved than creation or saving. What we are doing
is creating an NSFetchRequest
, specifying the entity we wish to fetch and
then executing this request without specifying any other criteria (e.g.
filters on the data, or instructions on how to sort the retrieved records).
Core Data therefore hands us back all records for the Task entity.
With these three helper methods in place, now when we build and run our app we can create, persist and fetch our tasks:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
=
> #<TasksStore:0x8e23db0 @context=#<NSManagedObjectContext:0x9c2d440>>
4
(
main)
> bank
=
tasks_store.create_task(
"Go to the bank"
)
5
=
> #<Task_Task_:0x9c2e170>
6
(
main)
> t1
=
tasks_store.get_tasks.first
7
=
> #<Task_Task_:0x9c2e170>
8
(
main)
> t1.task_description
9
=
> "Go to the bank"
10
(
main)
> tasks_store.save_context
11
=
> true
12
(
main)
> exit
Now if we build and run the app again we find that we can still access the task that we saved–it has been persisted to the store:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
=
> #<TasksStore:0x8c4d310 @context=#<NSManagedObjectContext:0x8f3f610>>
4
(
main)
> t1
=
tasks_store.get_tasks.first
5
=
> #<Task_Task_:0x8eca300>
6
(
main)
> t1.task_description
7
=
> "Go to the bank"
8
(
main)
> exit
Prettier Model RepresentationsLet’s take a moment to make life easier for ourselves by making our tasks
more easily recognisable in the console. At the moment when we work with our
When a Ruby object is printed or otherwise asked to represent itself as a
string, the
|
Searching and Sorting
In most applications we will find ourselves wanting to be able to:
- Look up individual records
- Filter records by certain criteria based on the record attributes (e.g. find all tasks where completed=true)
- Sort the returned records based on their attributes (e.g. sorting tasks by the date that they were created)
In this section we will take each of these in turn and show how they can be implemented.
Looking up Individual Records
A common use case in an iOS app is to display a list of records retrieved from
Core Data in a UITableView
. When the view controller is loaded we will
generally load all records into an instance variable (e.g. an NSArray
)
within the view controller and provide access to these records via the usual
UITableView
delegate methods.
When a user taps a row and we wish to show a detail view for that record, we would typically instantiate the detail view controller and pass it the individual record from our local array rather than requesting it again from Core Data.
As such, there are not many scenarios in which you would typically be
retrieving one and only one record from Core Data. One scenario where you
might do so however is when you are working with nested contexts and you wish
to determine whether the Core Data NSManagedObject
that you have is still in
the store, or whether its attributes have changed.
At this stage we have not encountered nested contexts–we are dealing with only one context for our app and as such we don’t need to worry about instances changing in the store without our knowledge; we are only displaying one view controller at a time and each view controller is accessing the same context.
We will revisit this however when we begin working with nested contexts later in the book.
Avoid Frequent FetchesWhen using a relational database, in most cases it isn’t considered to be bad practice to retrieve a specific record (by its primary key) when it is needed. With Core Data however, even though it is (usually) backed by a relational database you will want to minimise the number of round trips to the store. This is especially true for iOS. When retrieving records, for the best performance you will want to greedily
load more than you might necessarily need, as accessing those records later
from memory will be much faster than making successive trips to the store. For
optimum performance you will need to find the fine line between loading too
much at once (large fetches can cause delays) and loading too little (causing
many subsequent small fetch requests later). Key to this is setting an
appropriate value for the batch size of any The result will be much better response times in your app when users interact with it. |
Filtering Records
We will often want to filter records as we retrieve them from the store. For our tasks app some common use cases could be to filter tasks by their status (completed, not completed), their priority and perhaps a text search on the description of the task itself. In this section we will investigate how we can implement these kinds of filters.
Filtering on Priority and Completion Status
Let’s extend our get_tasks
method to allow for optional filtering on the
task priority and its completion status. If we pass in nil
for either, or
don’t pass them in, then no filtering will occur. So a call to
get_tasks(priority: 1, completed: false)
will return all tasks at priority
level 1 which have not yet been completed, while a bare call to get_tasks
will return all tasks as before.
Replace the previous implementation of get_tasks
with this extended version:
1
def
get_tasks
(
opts
=
{})
2
defaults
=
{
priority
:
nil
,
completed
:
nil
}
3
opts
=
defaults
.
merge
(
opts
)
4
5
request
=
NSFetchRequest
.
alloc
.
init
6
request
.
entity
=
NSEntityDescription
\
7
.
entityForName
(
'Task'
,
8
inManagedObjectContext
:
@context
)
9
10
predicates
=
[]
11
12
unless
opts
[
:completed
].
nil?
13
completed
=
NSNumber
.
numberWithBool
(
opts
[
:completed
]
)
14
completed_pred
=
NSPredicate
.
predicateWithFormat
(
"completed == %@"
,
15
completed
)
16
predicates
<<
completed_pred
17
end
18
19
unless
opts
[
:priority
].
nil?
20
priority
=
NSNumber
.
numberWithInt
(
opts
[
:priority
]
)
21
priority_pred
=
NSPredicate
.
predicateWithFormat
(
"priority == %@"
,
22
priority
)
23
predicates
<<
priority_pred
24
end
25
26
if
predicates
.
count
>
0
27
# Create a compound predicate by ANDing together any predicates
28
# specified thus far.
29
compound_predicate
=
NSCompoundPredicate
\
30
.
andPredicateWithSubpredicates
(
predicates
)
31
request
.
setPredicate
(
compound_predicate
)
32
end
33
34
error_ptr
=
Pointer
.
new
(
:object
)
35
36
data
=
@context
.
executeFetchRequest
(
request
,
error
:
error_ptr
)
37
38
if
data
==
nil
39
raise
"[ERROR] Error fetching taskss:
#{
error_ptr
[
0
].
description
}
"
40
end
41
42
data
43
end
Breaking it down
- The method now takes a parameter
opts
which defaults to an empty hash. - We provide our defaults for this hash (in this case they are all nil) and
merge the two so that when you call
get_tasks
you can override these default values. - If a value for either
completed
orpriority
is provided then we create a filter predicate expressing the appropriate condition and attach it to the fetch request. - The predicates (of type
NSPredicate
) are created using a format string, with the arguments being passed in as objects (%@
). - As we are dealing with numeric types in both cases (a boolean is 1 or 0,
priorities are integers) we create and pass in an
NSNumber
when creating the predicates. - As we can only specify one predicate for a given
NSFetchRequest
we create a compound predicate which is made byAND
ing each of the predicates together.
Filtering on the Task Description
If we would like to add a search function to our app then we can extend our
get_tasks
method to support this using the same approach as before. The
principal difference will be that we will be using a different comparison
operator, CONTAINS
rather than ==
.
First let’s extend our default arguments to include task_description
:
1
defaults
=
{
priority
:
nil
,
completed
:
nil
,
task_description
:
nil
}
Now we add our predicate, just below the priority predicate:
1
unless
opts
[
:task_description
].
nil?
2
task_description
=
opts
[
:task_description
]
3
task_description_predicate
=
NSPredicate
\
4
.
predicateWithFormat
(
"task_description CONTAINS[cd] %@"
,
5
task_description
)
6
predicates
<<
task_description_predicate
7
end
The CONTAINS
operator simply checks that the value for the attribute on the
left-hand side (task_description
) contains the argument on the right-hand
side. We could also have written the predicate using LIKE
and with wildcard
*
to get the same effect. The extra [cd]
options specify that we would
like the search to be case and diacritic insensitive which is generally what
you would like for a search function. There are many more operators and
options described in the official documentation on predicate
syntax.
Our get_tasks
method is now quite capable: we can retrieve all tasks or
filter on any combination of priority, completion status or search for a
fragment of a task description name.
Let’s try it out. First let’s add in some more tasks:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
(
main)
> tasks_store.get_tasks
4
=
> [
Priority: 5 Completed: 0 Description: 'Go to the bank'
]
5
(
main)
> tasks_store.create_task(
"Do the laundry and dry cleaning"
)
6
=
> Priority: 5 Completed: 0 Description: 'Do the laundry'
7
(
main)
> tasks_store.create_task(
"Go to the supermarket"
)
8
=
> Priority: 5 Completed: 0 Description: 'Go to the supermarket'
9
(
main)
> tax_return
=
tasks_store.create_task(
"Complete tax return"
)
10
=
> Priority: 5 Completed: 0 Description: 'Complete tax return'
11
(
main)
> tax_return.priority =
1
12
=
> 1
13
(
main)
> tax_return.completed =
1
14
=
> 1
15
(
main)
> tasks_store.save_context
16
=
> true
17
(
main)
> exit
We now have a selection of tasks with a mix of priorities and completion statuses. Let’s exercise the fetching and filtering code:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
=
> #<TasksStore:0x9086600 @context=#<NSManagedObjectContext:0x9155df0>>
4
(
main)
> tasks_store.get_tasks
5
=
> [
6
Priority: 5 Completed: 0 Description: 'Go to the bank'
,
7
Priority: 1 Completed: 1 Description: 'Complete tax return'
,
8
Priority: 5 Completed: 0 Description: 'Go to the supermarket'
,
9
Priority: 5 Completed: 0 Description: 'Do the laundry'
10
]
11
(
main)
> tasks_store.get_tasks.count
12
=
> 4
13
(
main)
> tasks_store.get_tasks(
priority: 1)
14
=
> [
Priority: 1 Completed: 1 Description: 'Complete tax return'
]
15
(
main)
> tasks_store.get_tasks(
priority: 1, completed: false
)
16
=
> []
17
(
main)
> tasks_store.get_tasks(
priority: 1, completed: true
)
18
=
> [
Priority: 1 Completed: 1 Description: 'Complete tax return'
]
19
(
main)
> tasks_store.get_tasks(
priority: 5)
.count
20
=
> 3
21
(
main)
> tasks_store.get_tasks(
priority: 5, task_description: "supermarket"
)
22
=
> [
Priority: 5 Completed: 0 Description: 'Go to the supermarket'
]
23
(
main)
> exit
Predicate Order MattersThe order in which predicates are specified can significantly affect the execution time of a fetch request. In general you will want to place non-textual predicates before textual predicates, and order predicates with the most restrictive predicates first. These two principles can conflict however, so you will need to think about the data you are storing and experiment. In the case of textual vs. non-textual predicates, it would be better to
specify Regarding more and less restrictive predicates, For more on this topic please see the official documentation. |
Updating Records
Updating records is straightforward: you simply retrieve the record
(encapsulated as an NSManagedObject
as usual), make changes to the
attributes as necessary, on as many objects as you like, and then save the
context. This is perhaps where the benefits of having the context start to be
apparent: you make all the changes you need to across all objects in the graph
and then persist when it is necessary or convenient to do so.
We have already had one example of this, when we were creating records with which to test our filtering. Here’s another where we retrieve, modify the description of and persist back two tasks. Note how straightforward this is:
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
=
> #<TasksStore:0x937d880 @context=#<NSManagedObjectContext:0x93b7b60>>
4
(
main)
> task_1, task_2
=
tasks_store.get_tasks()
.last(
2)
5
=
> [
#<Task_Task_:0xff9f640>, #<Task_Task_:0xff9f690>]
6
(
main)
> task_1.task_description
7
=
> "Go to the supermarket"
8
(
main)
> task_1.task_description =
"Go to the supermarket and chemist"
9
=
> "Go to the supermarket and chemist"
10
(
main)
> task_2.task_description
11
=
> "Do the laundry"
12
(
main)
> task_2.task_description =
"Do the laundry and dry cleaning"
13
=
> "Do the laundry and dry cleaning"
14
(
main)
> tasks_store.save_context
15
=
> true
Deleting Records
In the case where an object has no relationships to other objects then deletion is straightforward:
1
@context
.
deleteObject
(
aManagedObject
)
At this point the object still exists in the store however and won’t be deleted until the context is saved. In this manner you can create, edit and delete several objects in one go and have all of the changes persisted when you save, as long as validation passes on all the objects involved.
We can extend our TasksStore
to support deletion by adding a helper method
delete_task
as follows:
1
def
delete_task
(
task
)
2
@context
.
deleteObject
(
task
)
3
end
Deletion and RelationshipsWhen objects are related to each other then we will want to confirm that we have cleaned up any related objects that should no longer exist, and that any objects that should still exist are still consistent, i.e. they still pass validation in the absence of this relationship. |
Sorting Records
In retrieving your records you will often want to order the results, e.g. in alphabetical order, by priority, with completed tasks last and so on.
In our example application we would indeed like to order first by completed status (tasks that are due should come first), then by priority (highest to lowest) and then alphabetically by task description.
We can implement this by adding in three sort predicates into our fetch
request (in the get_tasks
method), one for each attribute:
1
completed_sort
=
NSSortDescriptor
.
alloc
.
initWithKey
(
"completed"
,
2
ascending
:
true
)
3
priority_sort
=
NSSortDescriptor
.
alloc
.
initWithKey
(
"priority"
,
4
ascending
:
true
)
5
description_sort
=
NSSortDescriptor
.
alloc
.
initWithKey
(
"task_description"
,
6
ascending
:
true
)
7
8
request
.
setSortDescriptors
(
[
completed_sort
,
9
priority_sort
,
10
description_sort
]
)
11
12
error_ptr
=
Pointer
.
new
(
:object
)
13
14
data
=
@context
.
executeFetchRequest
(
request
,
error
:
error_ptr
)
In each case we perform a simple sort in ascending order so that non-completed tasks are first (0 < 1), high priority (e.g. priority 1) tasks come before low priority tasks (e.g. priority 2-5) and finally when those attributes match we sort alphabetically on the task description.
Let’s see this in action. We will fetch our tasks, and then send the first task to the end of the list by completing the task (completed tasks should always come after tasks that are due):
1
$
rake
2
(
main)
> tasks_store
=
TasksStore.shared
3
=
> #<TasksStore:0x904b680 @context=#<NSManagedObjectContext:0x93d8a60>>
4
(
main)
> tasks_store.get_tasks
5
=
> [
Priority: 1 Completed: 0 Description: 'Complete tax return'
,
6
Priority: 5 Completed: 0 Description: 'Do the laundry and dry cleaning'
,
7
Priority: 5 Completed: 0 Description: 'Go to the bank'
,
8
Priority: 5 Completed: 0 Description: 'Go to the supermarket and chemist'
]
9
(
main)
> tax
=
tasks_store.get_tasks.first
10
=
> Priority: 1 Completed: 0 Description: 'Complete tax return'
11
(
main)
> tax.completed =
true
12
=
> true
13
(
main)
> tasks_store.save_context
14
=
> true
15
(
main)
> tasks_store.get_tasks
16
=
> [
Priority: 5 Completed: 0 Description: 'Do the laundry and dry cleaning'
,
17
Priority: 5 Completed: 0 Description: 'Go to the bank'
,
18
Priority: 5 Completed: 0 Description: 'Go to the supermarket and chemist'
,
19
Priority: 1 Completed: 1 Description: 'Complete tax return'
]
Wrapping Up
At this stage we are able to define entities in our data model from scratch, create, fetch, update and delete instances of them and persist these changes back to the store when we choose to do so. In order to create a fully-fledged production app however we still need to know about correct error handling, migrations and overall design patterns for accessing and working with our data, including the use of nested contexts.
Before we dive further into these topics however let’s first make life easier for ourselves by bringing in two third party libraries: one that lets us write our models in a simple schema language, and the other that will take what we have done so far and reduce it to one line of setup code.
At this point you may be wondering why we didn’t start with these libraries; the reason for this is that while these libraries can save us a lot of time, if we don’t understand how they work then when we inevitably encounter issues then we would be at the mercy of the (perhaps incomplete) documentation and StackOverflow answers, or else we would simply need to go back to the fundamentals to work out what went wrong. Starting with the fundamentals as we have done should, I hope, save you considerable time in the long run.
3. Tips & Troubleshooting
RubyMotion is still relatively new software and while it works well enough for production usage, in development you’re likely to bump into some quirks and possibly bugs.
Debugging Core Data
Increase the log level
1
$
rake args
=
"-com.apple.CoreData.SQLDebug 3 \
2
-com.apple.CoreData.MigrationDebug 1"
Can be set to 1, 2 or 3 for increasing amounts of debug information.
Core Data Error Codes
A full list of error codes is available in the Core Data Framework API docs.
Code 1570: Missing Mandatory Property
On attempting to save your context, you have a managed object in the context
that is missing a required attribute (i.e. it is set to nil
).
Look for NSValidationErrorKey
in the error output to find the offending
property. Now you need to trace through your code to find out why this
property is not being set.
1
"Error Domain=NSCocoaErrorDomain Code=1570
\"
The operation couldn't be
2
completed
.
(
Cocoa
error
1570.
)
\
" UserInfo=0x9826420
3
{
NSValidationErrorObject
=<
Task
:
0x8d77f90
>
(
entity
:
Task
;
id
:
0x8d7ad50
4
<
x
-
coredata
:
///Task/tAA6226D8-1347-4F26-925B-EE0C3FA3D6802> ; data: {\n
5
completed
=
nil
;
\
n
priority
=
nil
;
\
n
\
"task_description
\"
= nil;
\n
}),
6
NSValidationErrorKey
=
completed
,
NSLocalizedDescription
=
The
operation
7
couldn
'
t
be
completed
.
(
Cocoa
error
1570.
)}
"
In this case, a Task
in the context has no property values set at all. This
particular error refers to the completed
property but there will be similar
messages for the priority
and task_description
properties as well.
This collection of errors will be bundled along with an error of code 1560, which indicates multiple validation problems with the referenced managed object.
Code 134100: Current Schema is Incompatible with Current Store
You have updated your data model (schema) but you already have a persistent store which has been initialised with a schema that is not compatible with the schema you are now trying to load.
Look for the NSStoreModelVersionIdentifiers
key in the error message to see
what version the current data store is at.
To correct the situation you either need to:
- Revert your schema (if this was an accidental change),
- Delete the persistent store if you want to start afresh (e.g. run
rake simulator clean=1
), - Or migrate the data store. If you are using Core Data Query (CDQ) then follow the steps here, otherwise if you are using pure Core Data you can follow the steps here instead.
Interacting With Views in the Console
One of the key features of RubyMotion is the REPL (Read-Execute-Print-Loop, or console), which makes it easy to interact with your program while it is running. For example, you could add new elements to a view or tweak their attributes until you are happy–certainly much faster than repeatedly changing and hitting ‘Build and Run’ as would be required if we were using Xcode.
One issue you will hit right away though is that there isn’t an easy way to quickly refer to the current view. The following trick is a little ‘dirty’ and you will need to take care to remove such lines from your code after debugging but you can make the current view available via a global variable for easy access in the console.
For example if we have a view controller DetailViewController
:
1
class
DetailViewController
2
def
viewDidLoad
3
$this_view
=
self
4
5
@textField
=
UITextField
.
alloc
.
initWithFrame
(
[[
20
,
84
]
,
[
280
,
31
]]
)
6
end
7
end
Now when this view controller has loaded you can access it easily in the console:
1
=>
$
this_view
.
instance_variable_get
(
'@
textField
'
).
frame
2
=>
#
<
CGRect
origin
=
#
<
CGPoint
x
=
20.0
y
=
84.0
>
3
size
=
#
<
CGSize
width
=
280.0
height
=
31.0
>>
Note how we use instance_variable_get
to access the instance variable
@textField
; we could now for example tweak the origin or size of the frame,
alter its attributes, or anything else we would like to do.
Uninitialized constant NSSQLiteStoreType
If you start a new RubyMotion project and forget to add the Core Data framework to your Rakefile before you build/run the app then even if you update your Rakefile you are still likely to get NameError exceptions for Core Data types, e.g.
1
***
Terminating
app
due
to
uncaught
exception
'
NameError
'
,
2
reason:
'
app_delegate
.
rb
:
19
:
in
`
initialise_core_data_stack
'
:
3
uninitialized
constant
AppDelegate
::
NSSQLiteStoreType
(
NameError
)
The solution in almost all cases should just be that you need to run rake
clean
whenever you change the list of frameworks you load into your app. On
the next build all names should be resolved as you would expect.
Undefined method `viewWillAppear’
This error could occur, and cause a crash in your application, if you have
attempted to override a method such as viewWillAppear
as follows:
1
class
MyViewController
<
UIViewController
2
def
viewWillAppear
3
# Your code
4
end
5
end
This can cause the RubyMotion runtime to complain that MyViewController
has
no viewWillAppear
method. As you have just defined one that doesn’t sound
quite right.
What the error message is really saying is that it can’t find
viewWillAppear
, and the reason for that is that our definition above is
shadowing the one that the runtime would like to invoke, because it is
looking for viewWillAppear(animated)
.
As such, to fix this we should be sure to override viewWillAppear(animated)
and not the bare method viewWillAppear
, thus changing our code to be:
1
class
MyViewController
<
UIViewController
2
def
viewWillAppear
(
animated
)
3
# Your code
4
end
5
end
Other Errors and Crashes
Have you updated RubyMotion? If so, be sure to run rake clean
in the shell
before continuing.
If you are encountering errors which you can find no reference to online you
might have discovered a bug; check the RubyMotion changelog for a reference
to it and try updating to the latest version (followed by a rake clean
) to
see if that fixes it.
If you are still having problems then there is the RubyMotion Google Group, RubyMotion Support or you are very welcome to get in touch with me and I will do my best to help.
- One caveat is that RubyMotion is essentially an implementation of Ruby 1.9 which integrates tightly with iOS and OSX. As such you may miss some features that Ruby 2.x brings, and it has also been customised slightly to make working with the Cocoa libraries easier.↩
- This is a convention of Ruby rather than RubyMotion so you can use this in any of your own classes to customise their initialisation behaviour.↩