9781449380373
_core_data.html

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”.

Figure 7.1. Movie library using Core Data

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.

Figure 7.2. View of a blank xcdatamodel file in Xcode 4

View of a blank xcdatamodel file in Xcode 4

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).

Note

The Data Model Inspector is available in the Utilities area. If you can’t see it, you can display it via the top menu: View Utilities 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.

Figure 7.3. Movies’ attributes in Xcode 4

Movies’ attributes 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”.

Figure 7.4. Relationships between our entities

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.

Figure 7.5. The Movie/Actor relationship viewed from the Movie entity

The Movie/Actor relationship viewed from the Movie entity

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:

  1. Drag and drop an NSArrayController instance to the Object list, in the editor.

  2. 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.

  3. Change the Array Controller’s label to “movies” (use the identity label field in Identity Inspector)

  4. Finally, edit the movies array controller binding’s parameters to bind the Managed Object Context to the AppDelegate class. The Model Key Path should be automatically set to managedObjectContext, as shown in Figure 7.7, “Array controller bindings property set”.

Figure 7.6. Array controller attributes property set

Array controller attributes property set

Figure 7.7. Array controller bindings property set

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.

Figure 7.8. Actors’ content set bindings

Actors’ content set bindings

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”.

Figure 7.9. The finished UI

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:

  1. Drag and drop a Table View into your main window.

  2. Expand the Scroll View - Table View tab to access its Table View.

  3. Change the attributes (no headers, 1 column, source list highlight) to match Figure 7.10, “Table View attributes for Movies”.

  4. Expand the Table View to access its Table Column

  5. Bind the table view’s value to movies.arrangedObjects.title, as shown in Figure 7.11, “Table Column’s bindings”.

  6. Change the attributes to have a sort key and a selector, as shown in Figure 7.12, “Table Column’s attributes”.

Figure 7.10. Table View attributes for Movies

Table View attributes for Movies

Figure 7.11. Table Column’s bindings

Table Column’s bindings

Figure 7.12. Table Column’s attributes

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”.

Figure 7.13. Title’s text field binding

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).

Figure 7.14. Hardcoded Combo Box items

Hardcoded Combo Box items

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:

  1. Add two buttons to the UI (the style doesn’t matter).

  2. Edit the first button’s attributes to use the NSAddTemplate image and a “Round Rect” bezel.

  3. Edit the second button’s attributes to use the NSRemoveTemplate image and a Round Rect bezel.

  4. Select both buttons. Click Editor Size to Fit.

  5. Set the + button’s selector to the movies array controller add: action.

  6. Set the - button’s selector to the movies array controller remove: 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")
  }

end

Let’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))handler

If 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”.

Figure 7.15. Image View bindings

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:

  1. Add a new button

  2. Wire the button action to our add_image: method as shown Figure 7.16, “Image Button Action binding”.

Figure 7.16. Image Button Action binding

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.

Figure 7.17. Top part of the UI

Top part of the UI

Let’s focus next on the bottom part of the UI (Figure 7.18, “Bottom part of the UI”).

Figure 7.18. Bottom part of the UI

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.

Figure 7.19. Bindings for the Actor’s name column

Bindings for 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”).

Figure 7.20. Table View columns settings

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”).

Figure 7.21. Changing the class of a cell

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.

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.

Figure 7.22. Search field bindings

Search field bindings

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.

Note

You can set multiple predicates, and the user can use the arrow in the search field to choose the desired predicate. If you want to do that, you need to make sure to set a Display Name value so the user knows how he is filtering the data.

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”).

Figure 7.23. Contents of the Resources directory

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.

Site last updated on: November 9, 2011 at 10:00:57 AM PST
Cover for MacRuby: The Definitive Guide

View 1 comment

  1. colinta – Posted April 8, 2012

    should be "01/01/1895" (not "01/011/1895")

Add a comment