Chapter 8. Core Data–Related Cocoa Features

WHAT'S IN THIS CHAPTER?

  • Setting and getting data using key-value coding

  • Implementing a loosely coupled application architecture with messaging and key-value observing

  • Creating predicates in several ways and using them to filter data

  • Building sort descriptors to order your data structures

In the last three chapters, you learned about the fundamentals of the Core Data architecture — how to model your data and how to build a complete data-centric application using the Core Data framework. This chapter provides a more detailed look at some of the Cocoa functionality that you used with Core Data. In addition to their application in Core Data, you can use these features in other interesting ways.

In this chapter, you learn more about some important Cocoa technologies: key-value coding, key-value observing, predicates, and sort descriptors.

While you have seen these features used with Core Data, they are an integral part of the Cocoa framework. You can use these concepts and their associated classes in ways that reach far beyond Core Data. For example, you can use predicates and the NSPredicate class to filter and query regular Cocoa data structures such as arrays and dictionaries. You can develop loosely coupled, message-based application architectures using the concepts of key-value coding and key-value observing. Adding a deeper knowledge of these Cocoa features will broaden your knowledge of the development platform and provide you with more tools for your developer toolbox.

KEY-VALUE CODING

You have already seen key-value coding, also referred to as KVC, in previous chapters. When you used Core Data with an NSManagedObject directly, instead of using an NSManagedObject custom subclass, you used KVC to set and get the attribute values stored in the NSManagedObject. KVC allowed you to get and set the attributes of the managed object by name instead of using properties and accessor methods.

The term "key-value coding" refers to the NSKeyValueCoding protocol. This informal protocol specifies a way to access an object's properties using a name or key rather than by calling the accessor method directly. This capability is useful when you are trying to write generic code that needs to operate on different properties of different objects. For example, in Chapter 7, you designed the EditTextController as a generic controller that you can use to provide a text-editing capability. If you recall, you used this controller class to edit text attributes with different names in two different objects. The EditTextController used KVC to specify the appropriate text field name for the object that you wanted to edit.

Keys and Keypaths

Keys are the strings that you use to reference the properties of an object. The key is generally also the name of the accessor method used to access the property.

Properties and accessor methods are closely related. When you type @property NSString* name, you are telling the Cocoa framework to create accessor methods for the name property for you. The @synthesize directive that you use in your implementation file causes Cocoa to actually create the methods. The framework automatically creates the -(NSString*)name getter method and the -(void) setName:(NSString*)newName setter method. You can choose to override one or both of these methods if the default implementation does not meet your needs. It is a general standard that property names start with a lowercase letter.

To get a specific value from an object using KVC, you call the -(id)valueForKey:(NSString *)key method. This method returns the value in the object for the specified key. The valueForKey method returns the generic id type. This means that it can return any Objective-C object type, which makes KVC ideal for writing generic code. The method is unaware of the type of object that it will return and can therefore return any type.

If an accessor method or instance variable with the key does not exist, the receiver calls the valueForUndefinedKey: method on itself. By default, this method throws an NSUndefinedKeyException, but you can change this behavior in subclasses.

Instead of passing a simple key, there are alternate methods that allow you to use keypaths to traverse a set of nested objects using a dot-separated string. In the example in the previous chapter, you used keypaths to access a Task's Location's name property. When addressing a Task object, you used the keypath location.name to get to the name property from the location attribute of the Task object (see Figure 8-1).

Accessing a value with a keypath

Figure 8.1. Accessing a value with a keypath

The first key in the keypath refers to a field in the receiver of the call. You use subsequent keys to drill down into the object returned by the first key. Therefore, when used with a Task object, the keypath location.name gets that Task's location property and then asks that object for its name property. As long as keys in the keypath return references to objects, you can drill down as deeply into an object hierarchy as you wish.

You can use key-value coding to retrieve values from an object using a keypath instead of a simple string key by calling the -(id)valueForKeyPath:(NSString *)keyPath method. This works like valueForKey in that it returns the value at the given keypath. If anywhere along the keypath a specified key does not exist, the receiver calls the valueForUndefinedKey: method on itself. Again, this method throws an NSUndefinedKeyException, and you can change this behavior in subclasses.

Finally, the -(NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys method can be used for bulk retrieval of values using KVC. This method accepts an NSArray of keys and returns an NSDictionary with the keys as the keys in the dictionary and the values returned from the object as the values.

Setting Values Using Keys

Just as you can retrieve values from an object using KVC, you can set values using KVC. You call the -(void)setValue:(id)value forKey:(NSString *)key method to set the value for a specified key. If an accessor method or instance variable with the key does not exist, the receiver calls the setValue:forUndefinedKey: method on itself. By default, this method throws an NSUndefinedKeyException, but you can change this behavior in subclasses.

You can also use a keypath to set a value in a target object using the -(void)setValue:(id)value forKeyPath:(NSString *)keypath method. If any value in the keypath returns a nil key, the receiver calls the setValue:forUndefinedKey: method on itself. Again, this method throws an NSUndefinedKeyException, but you can change this behavior in subclasses.

You can set a group of values in an object using KVC by calling the -(void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues method. This method sets all of the values on all of the key objects given in the dictionary. Behind the scenes, this method simply calls setValue:forKey: for each item in the dictionary.

Collection Operators

If your object contains an array or set property, it is possible to perform some functions on the list. You can include a function in the key path in a call to valueForKeyPath. These functions are called collection operators. You call collection operators using the form [email protected].

The functions that you can use in a collection operator are:

  • @avg: Loops over each item in the collection, converts its value to a double and returns an NSNumber representing the average

  • @count: Returns the number of objects in the collection

  • @distinctUnionOfArrays: Returns an array containing the unique items from the arrays referenced by the keypath

  • @distinctUnionOfObjects: Returns the unique objects contained in the property

  • @max and @min: Return the maximum and minimum values respectively of the specified property

  • @sum: Loops over each item in the collection, converts its value to a double, and returns an NSNumber representing the sum

  • @unionOfArrays, @unionOfObjects, and @unionOfSets: Function just like their distinct counterparts except they return all items in the collection, not just unique items

For further information on using these functions, see Apple's Key-Value Coding Programming Guide, which is included as part of the Xcode documentation set.

Additional Considerations When Using KVC

It makes no difference if you access the properties of a class by using the dot syntax, calling the accessor method, or by using KVC. You can see this illustrated in Figure 8-2. You are calling the receiver's accessor method either way. You should be aware, however, that because there is an added level of indirection when using KVC, there is a slight performance hit. The performance penalty is very small, so you should not let this deter you from using KVC when it helps the flexibility of your design.

Accessing a value using different semantics

Figure 8.2. Accessing a value using different semantics

When building your own classes, you should pay attention to the naming conventions used by the Cocoa framework. Doing so helps to ensure that your classes will be key-value coding–compliant. For example, the correct format for accessor methods is -var for the getter and -setVar for the setter. Defining properties in your classes ensures that the accessor methods generated by the framework will be KVC-compliant.

There are additional rules for ensuring KVC compliance when your classes contain To-One or To-Many relationships. You should consult the Key Value Coding Programming Guide in the Apple docs for more detail on ensuring KVC compliance.

The valueForKey: and setValue:forKey: methods automatically wrap scalar and struct data types in NSNumber or NSValue classes. So, there is no need for you to manually convert scalar types (such as int or long) into Objective-C class types (such as NSNumber); the framework will do it for you automatically.

KEY-VALUE OBSERVING

In addition to obtaining property values using strings, you can take advantage of the NSKeyValueCoding protocol to implement another very powerful feature in Cocoa, key-value observing (or KVO). Key-value observing provides a way for objects to register to receive notifications when properties in other objects change. A key architectural feature of this functionality is that there is no central repository or server that sends out change notifications. When implementing KVO, you link observers directly to the objects that they are observing without going through an intermediary server. If you need to implement a centrally stored publish/subscribe capability, the NSNotification class provides this capability.

The base class for most Objective-C objects, NSObject, provides the basic functionality of KVO. You should generally not have to override the base class implementation in your own implementations. Using KVO, you can observe changes to properties, To-One relationships and To-Many relationships. By inheriting from NSObject, the base class implements KVO automatically on your objects. However, it is possible to disable automatic notifications or build your own manual notifications.

Observing Changes to an Object

To receive notifications for changes to an object, you must register as an observer of the object. You register your class as an observer by calling the addObserver:forKeyPath:options:context: method on the object that you want to observe.

The Observer parameter specifies the object that the framework should notify when the observed property changes. The KeyPath parameter specifies the property that you want to observe. Changes to this property will cause the framework to generate a notification. The options parameter specifies if you want to receive the original property value (NSKeyValueObservingOptionOld) or the new property value (NSKeyValueObservingOptionNew). You can receive both if you pass in both NSKeyValueObservingOptionOld and NSKeyValueObservingOptionNew using the bitwise OR operator. The context parameter is a pointer that the observed object passes back to the observer when a change occurs.

When the property that you are observing changes, the observer will receive a notification. Notifications come back to the observer through calls to the observer's observeValueForKeyPath:ofObject:change:context: method. The observed object calls this method on the observer when an observed property changes. Therefore, all observers must implement observeValueForKeyPath:ofObject:change:context: to receive KVO callbacks. You can see the relationship between the two objects along with the methods used to set up the relationship in Figure 8-3.

The KVO relationship

Figure 8.3. The KVO relationship

When the observed object calls observeValueForKeyPath:ofObject:change:context:, the observer receives a reference to the object that changed. Also sent to the receiver are the keypath to the property that changed, a dictionary that contains the changes, and the context pointer that you passed in the call that set up the relationship.

The NSDictionary that you receive in the callback contains information about the changes to the observed object. Depending on the options that you specified in the call to set up the observer, the dictionary will contain different keys. If you specified NSKeyValueObservingOptionNew, the dictionary will have an entry corresponding with the NSKeyValueChangeNewKey key that contains the new value for the observed property. If you specified NSKeyValueObservingOptionOld, the dictionary will have an entry for the NSKeyValueChangeOldKey key that contains the original value of the observed property. If you specified both options using a bitwise OR, both keys will be available in the dictionary. The dictionary will also contain an entry for the NSKeyValueChangeKindKey that gives you more information describing what kind of change has occurred.

When you are no longer interested in observing changes on an object, you need to unregister your observer. You accomplish this by calling the removeObserver:forKeyPath: method on the observed object. You pass the observer and the keypath to the property that the observer was observing. After you make this call, the observer will no longer receive change notifications from the observed object.

Automatic and Manual Implementations of KVO

The NSObject base class provides an automatic key-value observing implementation for all classes that are key-value coding compliant. You can disable automatic support for KVO for specific keys by calling the class method automaticallyNotifiesObserversForKey:. In order to disable keys, you need to implement this method to return NO for the specific keys that you do not want the framework to automatically support.

You can implement manual KVO notifications for finer control of when notifications go out. This is useful when you have properties that could change very often or when you want to batch many notifications into one. First, you have to override the automaticallyNotifiesObserversForKey: method to return NO for keys that you want to implement manually. Then, in the accessor for the property that you want to manually control, you have to call willChangeValueForKey: before you modify the value, and didChangeValueForKey: afterwards.

Key-Value Observing Example

Now that you are familiar with the concepts behind key-value coding and key-value observing, let's work through a simple example. The example will help to demonstrate how to use this functionality in practice. In this example, you will implement an iPhone version of a baseball umpire's count indicator. Umpires use this device to keep track of balls, strikes, and outs.

The sample application will use KVC and KVO to decouple the data object (Counter) from the interface (UmpireViewController). Even though this example is simplified, it will demonstrate how to use KVC and KVO to decouple your data objects from your interface. Keep in mind that this example is somewhat contrived in order to demonstrate using the principals of KVO and KVC in an application. You could easily implement this solution in many other, simpler ways, without using KVO and KVC. The UmpireViewController will use KVC to set the values for balls, strikes, and outs in the Counter object. The UmpireViewController will also observe changes for these fields and use the observation method to update the user interface.

Building the User Interface

The first task is to create the user interface in Interface Builder. Start a new project using the View-based Application template. Call your new application Umpire.

You should lay the interface out as shown in Figure 8-4. Open the UmpireViewController.xib interface file. Add three UIButton objects to the interface and change their titles to "balls," "strikes," and "outs." Add three UILabel objects, one above each button. Change the text in each one to the number 0.

Umpire application user interface

Figure 8.4. Umpire application user interface

Next, you need to modify the UmpireViewController.h header file to add outlets for the UI controls and an action method that will handle the action when the user taps on one of the buttons.

Open the UmpireViewController.h header file. Add an instance variable for each UILabel inside the @interface declaration:

UILabel* ballLabel;
UILabel* strikeLabel;
UILabel* outLabel;
                                                         
Umpire application user interface

Next, add properties for the three labels:

@property (nonatomic, retain) IBOutlet UILabel* ballLabel;
@property (nonatomic, retain) IBOutlet UILabel* strikeLabel;
@property (nonatomic, retain) IBOutlet UILabel* outLabel;
                                                         
Umpire application user interface

Finally, add an action method that you will call when a user taps one of the buttons:

-(IBAction)buttonTapped:(id)sender;

Now you need to open the UmpireViewController.m implementation file. At the top, synthesize the properties that you just declared in the header file:

@synthesize ballLabel,strikeLabel, outLabel;

Add a stub method for buttonTapped:

-(IBAction)buttonTapped:(id)sender {
    NSLog(@"buttonTapped");
}

Now you need to go back into Interface Builder and connect your outlets and actions. Open the UmpireViewController.xib interface file. Connect the ballLabel, strikeLabel, and outLabel outlets of the File's Owner to the appropriate label in the view. Next, wire up each button in the interface to the buttonTapped action of File's Owner. Your user interface is complete. Save the file and close Interface Builder.

Build and run the application. It should build successfully with no errors or warnings. In the iPhone simulator, tap each button and verify that when you tap each button, you see the "buttonTapped" message in the console log.

The Counter Data Object

Now that your user interface is set up and working correctly, you will build a data object to hold the umpire data. Create a new class that is a subclass of NSObject and call it Counter.

Open the Counter.h header file. Add three NSNumber instance variables, one each for balls, strikes, and outs:

NSNumber* balls;
NSNumber* strikes;
NSNumber* outs;
                                                         
The Counter Data Object

Add property declarations for each of the instance variables:

@property (nonatomic, retain) IBOutlet NSNumber* balls;
@property (nonatomic, retain) IBOutlet NSNumber* strikes;
@property (nonatomic, retain) IBOutlet NSNumber* outs;
                                                         
The Counter Data Object

In the implementation file Counter.m, synthesize the properties:

@synthesize balls,strikes,outs;

You will be using this object in the UmpireViewController so you need to add a reference to the Counter.h header file in the UmpireViewController.h header. Open the UmpireViewController.h header file and add an import statement to import the Counter.h header:

#import "Counter.h"

Also in the UmpireViewController.h header file, add a Counter instance variable:

Counter* umpireCounter;

Next, add a property called umpireCounter:

@property (nonatomic, retain) Counter* umpireCounter;

The umpireCounter variable will hold the instance of the Counter that you will use to keep track of the ball and strike count.

Finally, in the UmpireViewController.m implementation file, synthesize the new umpireCounter property:

@synthesize umpireCounter;

Implementing Key-Value Observing

With the data object in place, you can now connect your View Controller to the data object using key-value observing. Open the UmpireViewController.m implementation file.

You need to initialize the umpireCounter variable and set up the KVO observation in the viewDidLoad method. Here is viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];
    Counter *theCounter = [[Counter alloc] init];

    self.umpireCounter = theCounter;

    // Set up KVO for the umpire counter
    [self.umpireCounter addObserver:self
                         forKeyPath:@"balls"
                            options:NSKeyValueObservingOptionNew
                            context:nil];
    [self.umpireCounter addObserver:self
                         forKeyPath:@"strikes"
                            options:NSKeyValueObservingOptionNew
                            context:nil];
    [self.umpireCounter addObserver:self
                         forKeyPath:@"outs"
                            options:NSKeyValueObservingOptionNew
                            context:nil];

    [theCounter release];

}
                                                         
Implementing Key-Value Observing

First, as usual, you call the superclass implementation of viewDidLoad to ensure that the object is set up properly and ready for use. Next, you create an instance of a Counter object and assign it to the umpireCounter property.

The next section sets up the KVO observation for each of the balls, strikes, and outs properties of the Counter object. Let's take a closer look at the call to set up the observer for the balls property:

[self.umpireCounter addObserver:self
                     forKeyPath:@"balls"
                        options:NSKeyValueObservingOptionNew
                         context:nil];

Remember that Counter inherits the addObserver:forKeyPath:options:context: method from NSObject. You are calling this method to configure the UmpireViewController as an observer of the umpireCounter object. Therefore, you pass self in as the object that will be the observer. This particular observer will be observing the balls property of the umpireCounter, so you pass the string "balls" in for the keypath. You don't really care what the old value of balls is; you are only interested in the new value when the value changes so you pass the NSKeyValueObservingOptionNew option in the method call. Finally, you set the context to nil because you do not need context data.

Finally, in the viewDidLoad method, you release the local variable theCounter because you incremented its retain count when you assigned it to the umpireCounter property.

Now that you've set up your code to become an observer, you need to implement the observeValueForKeyPath:ofObject:change:context: method that the observed object calls when observed properties change. Here is the method:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    //  change gives back an NSDictionary of changes
    NSNumber *newValue = [change valueForKey:NSKeyValueChangeNewKey];

    // update the appropriate label
    if (keyPath == @"balls") {
        self.ballLabel.text = [newValue stringValue];
    }
    else if (keyPath == @"strikes") {
        self.strikeLabel.text = [newValue stringValue];
    }
    else if (keyPath == @"outs") {
        self.outLabel.text = [newValue stringValue];
    }
}
                                                         
Implementing Key-Value Observing

Remember that every time any observed property in the umpireCounter changes, the umpireCounter will call this method. The first line retrieves the new value from the change dictionary for the property that changed. Next, you examine the keypath that the umpireCounter passes in to determine which property has changed. Then, you use that knowledge to set the text of the appropriate label.

In the viewDidUnload method, you need to remove the KVO observers. You also set the properties of the class to nil. Here is the code for viewDidUnload:

- (void)viewDidUnload {
    // Release any retained subviews of the main view.
    // Tear down KVO for the umpire counter
    [self.umpireCounter removeObserver:self
                            forKeyPath:@"balls"];

    [self.umpireCounter removeObserver:self
                            forKeyPath:@"strikes" ];

    [self.umpireCounter removeObserver:self
                            forKeyPath:@"outs" ];

    self.ballLabel = nil;
    self.strikeLabel = nil;
    self.outLabel = nil;
    self.umpireCounter = nil;

    [super viewDidUnload];
}
                                                         
Implementing Key-Value Observing

Once again, you make a call to the umpireCounter object. This time, you call the removeObserver:forKeyPath: method to remove your class as an observer of the umpireCounter. You call this method once for each property that you are observing, passing self as the observer each time.

Then, you set each property to nil and call the superclass implementation of viewDidUnload.

While you are writing cleanup code, implement the dealloc method to release all of your instance variables and call the superclass dealloc method:

- (void)dealloc {
    [ballLabel release];
    [strikeLabel release];
    [outLabel release];
    [umpireCounter release];

    [super dealloc];
}
                                                         
Implementing Key-Value Observing

Updating Values with Key-Value Coding

The last thing that you need to do is implement the buttonTapped method that executes each time the user taps one of the buttons in the interface. Instead of specifically setting the property values of the umpireCounter using dot notation, you will use KVC in conjunction with the title of the button that was pressed to set the appropriate value. You also need to implement some business logic to limit the ball counter to a maximum of 3 and the strike and out counters to a maximum of 2. Here is the code for the buttonTapped method:

-(IBAction)buttonTapped:(id)sender {

    UIButton *theButton = sender;
    NSNumber *value = [self.umpireCounter valueForKey:theButton.currentTitle];

    NSNumber* newValue;

    // Depending on the button and the value, set the new value accordingly
    if ([theButton.currentTitle compare:@"balls"] == NSOrderedSame &&
        [value intValue] == 3) {

        newValue = [NSNumber numberWithInt:0];
    }
    else if (([theButton.currentTitle compare:@"strikes"] == NSOrderedSame ||
              [theButton.currentTitle compare:@"outs"] == NSOrderedSame )&&
              [value intValue] == 2) {

        newValue = [NSNumber numberWithInt:0];
    }
    else
    {
        newValue = [NSNumber numberWithInt:[value intValue]+1];
    }

    [self.umpireCounter setValue:newValue forKey:theButton.currentTitle];
}
                                                         
Updating Values with Key-Value Coding

First, you get a reference to the button that the user pressed to trigger the call to buttonTapped. Next, you use the title of that button as the key in a call to valueForKey to get the current value of that attribute from the umpireCounter. For example, if the user tapped the "balls" button, you are passing the string balls into the valueForKey method. This method will then retrieve the balls property of the umpireCounter. This method will work as long as the titles in the buttons match the property names in the data object.

The next line declares a new NSNumber that you will use to hold the value that you want to send back to the umpireCounter.

Next, you apply some business logic depending on which button the user pressed. If he pressed the "balls" button, you check to see if the old value was 3, and if it was, you set the new value back to 0. It does not make any sense for the balls counter to go higher than 3 because in baseball, 4 balls constitute a walk and the next batter will come up, erasing the old count.

The next line does a similar comparison for the strikes and outs counters, but you compare these values to 2. Again, values greater than 2 make no sense for each of these properties.

If you do not need to reset the particular counter back to zero, you simply increment the value and store the new value in the local newValue variable.

Finally, you use KVC to set the new value on the umpireCounter using the currentTitle of the button that the user pressed as the key.

The application is now complete. You should be able to successfully build and run. When you tap one of the buttons, the application should set the properties of the counter object using KVC. It should then fire the KVO callback method and update the count labels on the interface using KVO. Notice how you never explicitly retrieved values from the umpireCounter using properties or valueForKey.

USING NSPREDICATE

In the previous chapter, you learned how you could use predicates with Core Data to specify the criteria for a fetch. In general, you can use predicates to filter data from any class, as long as the class is key-value coding compliant.

Creating Predicates

You can create a predicate from a string by calling the NSPredicate class method predicateWithFormat:. You can include variables for substitution at runtime just as you would with any other string formatter. One issue to be aware of when creating predicates using strings is that you will not see errors caused by an incorrect format string until runtime.

When creating a predicate by calling the predicateWithFormat method, you must quote string constants in the expression. For example, you see that you have to quote the string literal URGENT in this method call:

[NSPredicate predicateWithFormat:"text BEGINSWITH 'URGENT'"]

However, if you use a format string with variable substitution (%@), there is no need for you to quote the variable string. Therefore, you could create the previous predicate using this format string:

[NSPredicate predicateWithFormat:"text BEGINSWITH %@", @"URGENT"]

You can also use variable substitution to pass in variable values at runtime like this:

[NSPredicate predicateWithFormat:"text BEGINSWITH %@", object.valueToFilter]

If you try to specify a dynamic property name using a format string and %@, it will fail because the property name will be quoted. You need to use the %K (Key) substitution character in the format string to omit the quotes.

Say, for example, that you wanted to create a predicate at runtime but wanted the field that you are filtering on to be dynamic, along with the value that you want to filter. If you tried this code, it would be incorrect because the property that you are trying to filter on would be incorrectly quoted by using the %@ substitution character:

[NSPredicate predicateWithFormat:"%@ == %@", object.property, object.valueToFilter]

The correct syntax for this predicate is as follows:

[NSPredicate predicateWithFormat:"%K == %@", object.property, object.valueToFilter]

You are not limited to creating predicates with keys. You can also create a predicate using a keypath. With respect to a Task object, the predicate location.name == "Home" is perfectly legal.

In addition to using the predicateWithFormat method, you can create predicates directly using instances of the NSExpression object and NSPredicate subclasses. This predicate creation method makes you write a lot of code, but it is less prone to syntax errors because you get compile-time checking of the objects that you create. You may also get some runtime performance increase because there is no string parsing with this method as there is with the predicateWithFormat: method.

To create the predicate text BEGINSWITH 'URGENT' using NSExpressions and NSPredicate subclasses, you code it like this:

NSExpression *lhs = [NSExpression expressionForKeyPath:@"text"];
NSExpression *rhs = [NSExpression expressionForConstantValue:@"URGENT"];
NSPredicate *beginsWithPredicate =
    [NSComparisonPredicate predicateWithLeftExpression:lhs
                           rightExpression:rhs
                           modifier:NSDirectPredicateModifier
                           type:NSBeginsWithPredicateOperatorType
                           options:0];

As you can see, this is quite a bit more than the simple one line of code shown previously. However, when using this method you do get the benefit of compile-time type checking.

The final method for creating predicates is to use predicate templates with variable expressions. You saw this technique in the previous chapter when you used a predefined fetch request from your data model. With this method, you create your predicate template using either of the previously mentioned methods but with $VAR as variables in the predicate. When you are ready to use the predicate, you call the predicateWithSubstitutionVariables: method on the predicate passing in a dictionary that contains the key-value pairs of the substitution variables and their values.

Using Predicates

You can evaluate any object that is KVC-compliant against a predicate using the evaluateWithObject method of the predicate. YES is returned if the object passed in meets the criteria specified in the predicate.

For example, suppose that you build a predicate called thePredicate with the criteria text BEGINSWITH 'URGENT' as described previously. If you had a reference to a Task object called theTask that had a text attribute, you could call the function [thePredicate evaluateWithObject: theTask]. If the Task's text attribute started with the string URGENT the call to evaluateWithObject would return YES. You can see that this functionality has nothing to do with Core Data. Again, you can use predicates with any object that is KVC–compliant.

The NSArray class has a method called filteredArrayUsingPredicate: that returns a new filtered array using the supplied predicate. NSMutableArray has a filterUsingPredicate: method that filters an existing mutable array and removes items that don't match the predicate. You can also use filteredArrayUsingPredicate: with the mutable array to return a new, filtered NSArray.

SORT DESCRIPTORS

Like predicates, the use of sort descriptors is not limited to Core Data. You can use sort descriptors to sort other data structures such as arrays, as long as the values contained within the array are KVC compliant. As you may recall from the previous chapter, you use sort descriptors to specify how to sort a list of objects.

Sort descriptors specify the property to use when sorting a set of objects. By default, sorting using a sort descriptor calls the compare: method of each object under consideration. However, you can specify a custom method to use instead of the default compare: method.

Keep in mind that the descriptor doesn't do the sorting. The sort descriptor just tells the data structure how to sort. This is similar to how an NSPredicate doesn't actually do the filtering; it simply specifies how to filter.

The first step in using a sort descriptor is to initialize a sort descriptor with the key that you want to sort on. You also need to specify if you want to sort the resulting data in ascending or descending order. You initialize a sort descriptor by using the initWithKey:ascending: method.

Next, you create an array of descriptors by calling the NSArray arrayWithObjects: method and passing in one or more descriptors. You need to create an array because this allows you to sort on more than one field at a time. The framework applies the sort descriptors in the order that you specify them in the array.

For example, if you had an array of Task objects called theTaskArray, you could sort the array first on dueDate and then on text by creating an array containing two sort descriptors and calling the sortedArrayUsingDescriptors method:

// Create the sort descriptors
NSSortDescriptor *dueDateDescriptor =
    [[NSSortDescriptor alloc] initWithKey:@"dueDate" ascending:NO];
NSSortDescriptor *textDescriptor =
    [[NSSortDescriptor alloc] initWithKey:@"text" ascending:YES];

// Build an array of sort descriptors
NSArray *descriptorArray =
    [NSArray alloc arrayWithObjects: dueDateDescriptor, textDescriptor, nil];

// Sort the array using the sort descriptors
NSArray *sortedArray =
    [theTaskArray sortedArrayUsingDescriptors:descriptorArray];

The sortedArrayUsingDescriptors method works by calling the compare: method on the type that you are sorting. If the compare method is not appropriate for your application, you can specify a different method to use when sorting by creating your sort descriptors with the initWithKey:ascending:selector: method.

Specifically, when comparing strings, Apple's String Programming Guide for Cocoa recommends that you use a localized string comparison. So instead of compare:, you should generally specify that the sort descriptor use the localizedCompare: or localizedCaseInsensitiveCompare: method using the @selector (localizedCaseInsensitiveCompare:) syntax.

Therefore, when sorting your Task objects based on the text field, which is a string, you should use a sort descriptor defined like this:

NSSortDescriptor *textDescriptor =
        [[NSSortDescriptor alloc]
            initWithKey:@"text"
            ascending:YES
            selector:@selector(localizedCaseInsensitiveCompare:)];

The localizedCaseInsensitiveCompare: method of the NSString class uses an appropriate localized sorting algorithm based on the localization settings of the device.

MOVING FORWARD

In this chapter, you learned how you can use some of the features of the Cocoa framework that you learned about in the context of Core Data, outside of Core Data.

You used key-value coding and key-value observing to build an application that has its user interface loosely coupled to its data model. Architecturally, loose coupling of application data and the user interface is generally a good thing.

Then you learned how to create predicates and use them to filter data in arrays. You also learned how you could use predicates to do an ad hoc comparison of an object to some specific criteria.

Finally, you learned how to create and apply sort descriptors to sort arrays.

You should now feel comfortable with using these features inside your Core Data–based applications. You should also be able to apply the same concepts and technologies to work with other data structures as well.

In the next chapter, you finish the exploration of the Core Data framework with a look at optimizing Core Data performance. You will also look at versioning your database and migrating existing applications from one database version to another.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset