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.
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 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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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;
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;
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.
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;
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;
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;
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]; }
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]; } }
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]; }
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]; }
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]; }
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
.
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.
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.
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
.
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.
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.