Chapter 7. Core Data
Core Data is Cocoa’s model and framework for manipulating and storing data. In a nutshell, Core Data provides a nice way to handle relational object persistence without having to worry about the underlying storage.
What do you get from it?
Key-value coding and key-value observing
Validation
Undo/redo support
Relationship maintenance
Querying, filtering, and grouping
Version tracking and optimistic locking
Schema migration
A memory-optimized solution
Integration with Apple’s tool chain (XCode, Interface Builder, and Instruments)
But remember that Core Data is not a database replacement. Even though you can set Core Data to use SQLite as a data store, Core Data doesn’t support sophisticated database operations such as joins. All it supports is the basic CRUD operations. See the Wikipedia entry for CRUD interface. If you are interested in using a database such as SQLite, look at the various Ruby ORMs such as Sequel.
Data Model
At the heart of Core Data is a rich data modeling solution based on simple tools and configuration. The modeling is pretty close to a database design and if you have any experience with databases, you should be able to adapt really quickly.
The best way to understand how Core Data works is to create a simple data model. Let’s build a simple example application: a movie library. At the end of this chapter, we will have built a persisting (saved to disk) movie library that looks like the one shown in Figure 7.1, “Movie library using Core Data”.
To get there, we are going to use Xcode.
It will dramatically reduce the amount of code we have to write. As a
matter of fact, we are probably going to write less than 50 lines of code
in total! Start Xcode and create a new MacRuby application called CoreDataExample, and don’t forget to
enable the Use Core Data option. This template handles persistence through
a file with the extension .xcdatamodel and an application delegate
class.
The Data Model and the Entity
An entity in Core Data is a bit like a container that structures the objects it holds. It can be loosely compared to a database table, but it differs in some major ways. For instance, entities may be arranged in an inheritance hierarchy and are directly tied to a class.
Let’s create an entity for our movies. Browse the model folder;
you should see a file called CoreDataExample.xcdatamodeld. Select this
file. You should see something similar to Figure 7.2, “View of a blank xcdatamodel file in Xcode 4”. As we build our entity, this file will
hold the data structure that Core Data will use.
Click Add Entity and name the new entity Movie.
We don’t need to change the defaults. The entity is represented by
the NSManagedObject class and doesn’t inherit from any other entities (see the
details in the Data Model Inspector).
Adding Attributes
An attribute is something describing data, such as a movie’s title. Attributes are to an entity as columns are to a table: until we add attributes, an entity is useless. Attributes can be added different ways and are edited using the Data Model Inspector. After selecting the entity, click the big Add Attribute button at the bottom of the screen. Add the following attributes with the appropriate settings:
title(string, required)genre(string, optional)imagePath(string, optional)release_date(date, optional, minimum value: 01/011/1895)
At this point, your model should look like Figure 7.3, “Movies’ attributes in Xcode 4” in Xcode 4.
Relationships
Relationships between entities are another very important aspect of model design, just as foreign keys provide relationships between database tables. An example of a relationship in our case is between a movie and actors. A movie can have many actors. So we’ll model this as a one-to-many relationship between a movie and an actor. (In real life, you’d have to do something more complicated, because an actor can also be in many movies, but we’ll stick to one-to-many for now.)
First, we’ll create a second entity, an Actor with the following attributes:
name(string)gender(string, default value: Female)fictional(Boolean, default value: NO)
We will now create a relationship between Movie and Actor, as illustrated in Figure 7.4, “Relationships between our entities”.
Select Movie and add a new
relationship called actors. Set the
destination to Actor— don’t worry
about the inverse relationship for now—and set the relationship to be
optional and plural (To-Many Relationship).
Now select Actor and create a
new relationship. The new relationship should be named actorMovie with Movie as a destination and actors as the inverse relationship. Make sure
the Optional checkbox is enabled and the Plural checkbox is not enabled.
You can then see the final relationship in Xcode as Figure 7.5, “The Movie/Actor relationship viewed from the Movie
entity”. Notice that the Movie’s inverse relationship has been set
automatically.
Setting Up Controllers
We’ll spend most of the rest of this chapter in what used
to be called Interface Builder, the UI editor for Xcode. Since
Xcode 4, Interface Builder is built into Xcode. Depending
on the version you are using, click or double-click the .xib file shown in the Xcode navigator. What
we want to do is create a UI that will let us create and edit existing
movies and their related data.
The first step is to create a UI object that will retrieve and
store all our movies. The best object to use for that is NSArrayController. This
class, which is compatible with Cocoa bindings (it conforms to a protocol allowing you to
automatically bind UI elements to objects), manages a collection of
objects and provides selection management and sorting capabilities. We will need two
array controllers—one for our movies and one for their actors:
Drag and drop an
NSArrayControllerinstance to the Object list, in the editor.Edit the controller’s attributes to look like Figure 7.6, “Array controller attributes property set”, by doing the following:
Change the mode from Class to Entity.
Name the entity
Movie.Make sure the Prepare Content flag is on.
Change the Array Controller’s label to “movies” (use the identity label field in Identity Inspector)
Finally, edit the
moviesarray controller binding’s parameters to bind the Managed Object Context to theAppDelegateclass. The Model Key Path should be automatically set tomanagedObjectContext, as shown in Figure 7.7, “Array controller bindings property set”.
Note
The Object Library, where you will find NSArrayController, is inside the Utility
area. You can display it via the top menu: View → Utilities →
Object Library. If you want a better view of the objects and
placeholders, click the small arrow at the bottom of the long column
of icons to the left of the UI preview.
We’ve finished with the Movie
entity. Now go through a similar process for Actor.
Create an NSArrayController
called actors with its mode set to
the Actor entity name and the Prepare
Content flag checked. However, this time the bindings will be slightly different. We want to bind the
actor’s controller to the movie’s actors so we don’t display all the
actors in memory, but only the ones related to the current edited movie.
To do that, bind the Content Set value to movies. Set the Controller Key to selection and the Model Key Path to actors, as shown in Figure 7.8, “Actors’ content set bindings”. Don’t forget to also
bind the Managed Object Context in the parameters subsection to the
AppDelegate class and the Model Key
Path to managedObjectContext.
User Interface
So far, we’ve designed our model and created two array containers to hold our data. It’s now time to add some UI elements. The end result should look like Figure 7.9, “The finished UI”.
Movies
We’ll start with the Table View, which will
list all the movies. The goal here is to allow sorting and selection of
movies:
Drag and drop a
Table Viewinto your main window.Expand the Scroll View - Table View tab to access its
Table View.Change the attributes (no headers, 1 column, source list highlight) to match Figure 7.10, “Table View attributes for Movies”.
Expand the
Table Viewto access itsTable ColumnBind the table view’s value to
movies.arrangedObjects.title, as shown in Figure 7.11, “Table Column’s bindings”.Change the attributes to have a sort key and a selector, as shown in Figure 7.12, “Table Column’s attributes”.
Now that we have a way to display our movie titles, we need a way
to display more information and to edit this information. We are going
to add three fields: one for the title, one for the release date, and
one for the genre. Assign a label to identify each field and use a
Text Field data type for the title, a
Date Picker for the release date, and
a Combo Box for the genre.
Each field’s value needs to be bound to the related array controller selection’s attribute. For instance, the title’s text field is shown in Figure 7.13, “Title’s text field binding”.
The Combo Box is slightly
different, since we need to define the available values. This can be
done in the Model, but to keep it simple, we are going to hardcode the
values, as shown in Figure 7.14, “Hardcoded Combo Box items” (don’t forget to bind
the box to movies.selection.genre).
We also need two buttons, one to add a new movie and one to remove an existing movie. The buttons’ actions must be bound to the movies array controller:
Add two buttons to the UI (the style doesn’t matter).
Edit the first button’s attributes to use the
NSAddTemplateimage and a “Round Rect” bezel.Edit the second button’s attributes to use the
NSRemoveTemplateimage and a Round Rect bezel.Select both buttons. Click Editor → Size to Fit.
Set the + button’s selector to the
moviesarray controlleradd:action.Set the - button’s selector to the
moviesarray controllerremove:action.
Note
To set the buttons’ selector, right-click a button, expand the Sent Actions section, click the round icon on the same line as the selector, drag the cursor to the array of your choice, and then choose the appropriate action.
Build and run the application to make sure it works fine. You should be able to add a new movie by clicking the + button, then select the new row and edit the attributes. Try it a few times to make sure everything works as expected.
Art Cover
What would a movie library be without art covers? We are going to write some code to handle the movie covers. We need to let our users choose a cover and we need to display it when the movie is selected. That will actually be the only code manually written in this chapter.
To give the user the option to add/change/display movie covers, we
need to start writing our action and wire it to our views. The wiring is
done through an outlet (IBOutlet)
defined in our AppDelegate class and
keeping a reference to our movies
NSArrayController.
MacRuby makes that really easy: just define an attribute writer or
attribute accessor, and Xcode will see it as an outlet. By default, the
MacRuby Core Data Application template defines an attribute accessor for
the main window. This creates a getter and a setter for the window,
allowing us to call it from within our code as window. We now need to add an accessor/outlet
for movies, the array controller we
have created in the view:
class AppDelegate attr_accessor :window attr_accessor :movies
Don’t forget to wire the AppDelegate movies outlet to the movies controller (right-click AppDelegate in the UI editor and connect the
two objects). We can now write the action code as follows:
def add_image(sender)
movie = movies.selectedObjects.lastObject
return unless movie
panel = NSOpenPanel.openPanel
panel.canChooseDirectories = false
panel.canCreateDirectories = false
panel.allowsMultipleSelection = false
panel.beginSheetModalForWindow window, completionHandler: Proc.new{|result|
return if (result == NSCancelButton)
path = panel.filename
# use a GUID to avoid conflicts
guid = NSProcessInfo.processInfo.globallyUniqueString
# set the destination path in the support folder
dest_path = applicationFilesDirectory.URLByAppendingPathComponent(guid)
dest_path = dest_path.relativePath
error = Pointer.new(:id)
NSFileManager.defaultManager.copyItemAtPath(path, toPath:dest_path, error:error)
NSApplication.sharedApplication.presentError(error[0]) if error[0]
movie.setValue(dest_path, forKey:"imagePath")
}
endLet’s break it down to make sure everything is clear:
movie = movies.selectedObjects.lastObject return unless movie
We start by fetching the selected movie from the movies NSArrayController. If nothing is selected, we
just exit the action.
Then we open a panel (see http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSOpenPanel_Class/Reference/Reference.html) and set its settings:
panel = NSOpenPanel.openPanel panel.canChooseDirectories = false panel.canCreateDirectories = false panel.allowsMultipleSelection = false
Finally, we call the beginSheetModalForWindow API, which takes a C
block as its last argument. If you need to refresh your memory
concerning C blocks, refer to the section called “Blocks”.
Here is the API call:
panel.beginSheetModalForWindow window, completionHandler: Proc.new{|result|
return if (result == NSCancelButton)
path = panel.filename
# use a GUID to avoid conflicts
guid = NSProcessInfo.processInfo.globallyUniqueString
# set the destination path in the support folder
dest_path = applicationFilesDirectory.URLByAppendingPathComponent(guid)
dest_path = dest_path.relativePath
error = Pointer.new(:id)
NSFileManager.defaultManager.copyItemAtPath(path, toPath:dest_path, error:error)
NSApplication.sharedApplication.presentError(error[0]) if error[0]
movie.setValue(dest_path, forKey:"imagePath")
}We are calling beginSheetModalForWindow on panel and passing two arguments: the Outlet
pointing to our window and a proc that is called when the modal is
closed. The proc takes an argument that reflects the button pressed by
the user. The Objective-C method signature of this API looks like this:
- (void)beginSheetModalForWindow:(NSWindow *)window
completionHandler:(void (^)(NSInteger result))handlerIf the handler is passed a result matching the constant value
NSCancelButton, the
user has changed his mind and we should exit the action. Otherwise, we
collect the selected filename from the panel. We also create a
destination path by appending a global unique ID to the applicationFilesDirectory path. applicationFilesDirectory, defined in the
application template, specifies where all the application’s files are
saved. In this case, the value of applicationFilesDirectory is
~/Library/Application
Support/CoreDataExample/.
Once we know where we want to save the file, we can copy it using
NSFileManager. We then
check that the error pointer doesn’t contain any errors. If it does, we
present them to the user via the NSApp.presentError
call. Finally, we set the imagePath
of our movie.
Now it’s time to wire our brand new action in the UI.
Start by dragging an Image Well (instance of NSImageView) from the
Object Library to the main window. This will display the art cover once
the user chooses it. Bind its Value Path to movies.selection.imagePath. In other words,
bind the Image Well’s Value Path to movies, and set the Controller Key to
“selection” and the Model Key Path to imagePath, as shown in Figure 7.15, “Image View bindings”.
Now that the bindings are set, the UI will display the image from our data store. But we still need to define a way for the user to use the code we just wrote and let him add or change a movie’s cover. If you’ve followed along carefully, you should be able to implement the next step on your own. But just in case, here it is:
Add a new button
Wire the button action to our
add_image:method as shown Figure 7.16, “Image Button Action binding”.
Your UI should look similar to what’s shown in Figure 7.17, “Top part of the UI”. We’ve built the table view, the various buttons, and the image preview, but we are missing the search box (we’ll work on that last) and the bottom part.
Let’s focus next on the bottom part of the UI (Figure 7.18, “Bottom part of the UI”).
Actors
The lower part of the UI shows the actors for a selected
movie. We are going to set a new Table
View exactly the same way as the movies’ table view, but
instead of binding its elements to the movies array controller, we are going to bind
them to the actors array
controller.
Because we just went through these steps, we won’t go into the binding details. You can look at the example source code if you can’t set the bindings properly.
Figure 7.19, “Bindings for the Actor’s name column” shows a screenshot of the actor’s name column.
Something you might not know how to do yet is to use a different type of column cell. As you can see in the example, the actors’ table uses three different cell types: Text Field Cell, Combo Box Cell, and Button Cell.
For that, you first need to add a new column, select the actors’ table view, and open the inspector. Once there, set the total amount of columns you want (Figure 7.20, “Table View columns settings”).
Then extend the newly added column, choose the cell, and open the
identity tab. In the Custom Class field pick another cell class such as
NSButtonCell,
NSComboBoxCell,
NSSliderCell, or NSImageCell (Figure 7.21, “Changing the class of a cell”).
You also need to bind the combo box’s value to actors.selection.gender and the button cell’s
value to actors.selection.fictional.
Search
The last missing part is the search field at the top right
of our UI. The search field allows us to search for movies by name. Drag
and drop the Search Field icon (representing the NSSearchField class)
from the Object Library to the UI. Once the field is positioned, we need
to set some bindings so the text entered in the text field is used to
search the movies by title.
As shown in Figure 7.22, “Search field bindings”, we need to bind the
search predicate value to the Movies
array controller. Apple has a lot of documentation about using
predicates (see http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Predicates/predicates.html)
In this example, we’ll just scratch the surface.
As you can see in Figure 7.22, “Search field bindings”, when binding the
search to the Movies array, we also
set the Controller Key value to filterPredicate (this is called a predicate binding). This means we are
going to set a predicate for an array controller to filter the content
array (in our case Movies). This
process is also well documented by Apple (see http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/CocoaBindingsRef/CocoaBindingsRef.html).
The last missing piece is the predicate format, which is a bit like the
query string. In our case, because we want to find any movies containing
the string entered in the search field, the predicate format looks like
title contains $value, where $value is replaced by the value of the text
field.
Persistence
Data persistence is handled for you by the template provided by MacRuby. Let’s look at the different parts involved in providing data persistence.
Managed Object Model
Before even talking about persisting the user data, we
need to talk about the Managed Object Model, also known as “mom.” When
we worked on the Core Data app, we designed our model by editing an
.xcdatamodeld file: CoreDataExample.xcdatamodeld. In the
background, Xcode actually uses a source directory where all the info is
stored. When we compile our app, each file in the source directory is
compiled into a mom file and stored into a folder
with the .momd extension (as in “mom
deployment” directory). This is how the application knows about the data
structures and relationships to use.
If you look at your compiled application (the .app file), right-click it, choose Show
Package Contents, and drill down to Contents/Resources, you will see the
momd folder and its mom file
(Figure 7.23, “Contents of the Resources directory”).
The template has a method that finds the momd deployment directory and exposes it to the rest of the code.
Note
The mom compiler is also available outside of Xcode, via the command line: /Developer/usr/bin/momc
Managed Object Context
The Managed Object Context is at the heart of the Core Data stack. The main job of the context is to manage a collection of objects. It is used under the covers to create and fetch managed objects, and to manage undo and redo operations.
All the data is stored in memory and is then flushed to the persistent store when the context is saved. The Managed Object Context is basically what you interact with when editing Core Data values. It is a very powerful layer that works great and that you don’t have to worry about when writing Core Data application using the MacRuby template. That’s because the template takes care of setting the context and making it available for you.
To summarize, the Managed Object Context handles the interactions with Core Data managed objects in a very transparent way and delegates the persistence to a persistent store via its coordinator.
Persistent Store Coordinator
The persistent store coordinator is an API on top of different types of persistent stores such as XML, SQLite, binary, or in-memory. The coordinator acts as a broker between one or many Managed Object Contexts and one or many persistent stores. In other words, they associate Managed Object Models to persistent stores via the use of the models’ contexts.
You can run multiple coordinators connecting to one or many
stores, depending on what you want to do. By default, the MacRuby
Core Data template sets only one coordinator, which uses
an XML store. If you look at the AppDelegate.rb file that is generated with
your Core Data app, you will notice the persistentStoreCoordinator method. Here is how
the store is set:
url = directory.URLByAppendingPathComponent("CoreDataExample.storedata")
@persistentStoreCoordinator = NSPersistentStoreCoordinator.alloc. \
initWithManagedObjectModel(mom)
@persistentStoreCoordinator.addPersistentStoreWithType(NSXMLStoreType,
configuration:nil,
URL:url,
options:nil,
error:error)You can easily change that default if you decide to use another store.
Workflow
Now that we have examined all the different moving pieces,
let’s see how they come together. In our interface, we bound our movies’
and actors’ array controllers to the AppDelegate’s Managed Object Context (see
Figure 7.7, “Array controller bindings property set”) and we mapped
these controllers to entities in our Managed Object Model. By wiring
these few things, we gained access to our model and its context. The
Xcode template defines a few other hooks such as the delegation of the
window undo manager to the Model Object Context, giving us “free” undo
and redo via the context. The template also defines the saveAction that commits the context
changes to the persistent store and a hook into the app termination
process that triggers the saving of the managed context to the
persistent store (see the applicationShouldTerminate method in the
AppDelegate.rb file).
The good news is that the wiring needed at the developer level is very simple and, unless you have custom needs, the defaults work fine. If you want to know the difference between the various persistent stores and why and how to run many persistent store coordinators, or if you want to dig further into Core Data, I strongly encourage you to look at the documentation provided by Apple on the topic.




























Add a comment



Add a comment