Chapter 6. Custom Managed Objects

At the moment, our Hero entity is represented by instances of the class NSManagedObject. Thanks to key value coding, we have the ability to create entire data models without ever having to create a class specifically designed just to hold our application's data.

There are some drawbacks to this approach, however. For one thing, when using key value coding with managed objects, we use NSString constants to represent our attributes in code, but these constants are not checked in any way by the compiler. If we mistype the name of an attribute, the compiler won't catch it. It can also be a little tedious, having to use valueForKey: and setValue:forKey: all over the place instead of just using properties and dot notation.

Although you can set default values for some types of data model attributes, you can't, for example, set conditional defaults such as defaulting a date attribute to today's date. For some types of attributes, there's no way at all to set a default in the data model. Validation is similarly limited. Although you can control certain elements of some attributes, like the length of a string, or max value of a number, there's no way to do complex or conditional validation, or to do validation that depends on the values in multiple attributes.

Fortunately, NSManagedObject can be subclassed, just like other Objective-C classes, and that's the key to doing more advanced defaulting and validation. It also opens the door to adding additional functionality to your entity by adding methods. You can, for example, create a method to return a value calculated from one or more of the entity's attributes.

In this chapter, we're going to create a custom subclass of NSManagedObject for our Hero entity, then we're going to use that subclass to add some additional functionality. We're also going to add two new attributes to Hero. One is the hero's age. Instead of storing the age, we're going to calculate it based on their birthdate. As a result, we won't need Core Data to create space in the persistent store for the hero's age, so we're going to use the Transient attribute type and then write an accessor method to calculate and return the hero's age. The Transient attribute type tells Core Data not to create storage for that attribute. In our case, we'll calculate the hero's age as needed at run time.

The second attribute we're going to add is the hero's favorite color. Now, there is no attribute type for colors, so we're going to implement something called a transformable attribute. Transformable attributes use a special object called a value transformer to convert custom objects to instances of NSData so they can be stored in the persistent store. We'll write a value transformer that will let us save UIColor instances this way. In Figure 6-1, you can see what the detail editing view will look like at the end of the chapter with the two new attributes in place. Notice that the row for Age doesn't have a disclosure indicator next to it. That's our users' clue that it's not an editable field.

The hero detail editing view as it will look at the end of the chapter

Figure 6.1. The hero detail editing view as it will look at the end of the chapter

Of course, we don't have an attribute editor for colors, so we'll have to write one of those to let the user select the hero's favorite color. We're just going to create a simple, slider-based color chooser (Figure 6-2).

Because there's no way to set a default color in the data model, we're going to write code to default the favorite color attribute to white. If we don't do that, then the color will be nil when the user goes to edit it the first time, which will cause problems.

Finally, we'll add validation to the date field to prevent the user from selecting a birthdate that occurs in the future and we're also going to tweak our attribute editors so that they notifiy the user when an entered attribute has failed validation. We'll give the user the option to go back and fix the attribute, or to just cancel the changes they made (Figure 6-3).

The color attribute editor that we will be building in this chapter

Figure 6.2. The color attribute editor that we will be building in this chapter

When attempting to save an attribute that fails validation, the user will have the option of fixing the problem, or cancelling their changes

Figure 6.3. When attempting to save an attribute that fails validation, the user will have the option of fixing the problem, or cancelling their changes

Although we're only going to be adding validation to the Birthdate field, the reporting mechanism we're going to write will be generic and reusable if you add validation to another field. You can see an example of our generic error alert in Figure 6-4.

Since our goal is generally to write reusable code, our validation mechanism will also enforce validations done on the data model, such as minimum length.

Figure 6.4. Since our goal is generally to write reusable code, our validation mechanism will also enforce validations done on the data model, such as minimum length.

There's a fair amount of work to do, so let's get started. We're going to continue working with the same SuperDB application from last chapter. Make sure that you created a new version of your data model and that you turned on lightweight migrations as shown in the last chapter.

Updating the Data Model

The first order of business is to add our two new attributes to the data model. Make sure that the disclosure triangle next to SuperDB.xcdatamodeld in the Resources folder is expanded, and single-click on the current version of the data model, the one with the green check mark icon on it.

Once the data model editor comes up, select the Hero entity by clicking either on the rounded rectangle in the diagram view or on the row labeled Hero in the entity pane (Figure 6-5).

Selecting the Hero entity so that we can add new attributes to it

Figure 6.5. Selecting the Hero entity so that we can add new attributes to it

Adding the Age Attribute

Click the plus icon in the lower left of the property pane and select Add Attribute to add a new attribute. Change the new attribute's name to age, then check the Transient check box. That will tell Core Data that we don't need to store a value for this attribute. In our case, since we're using SQLite for our persistent store, this will tell Core Data not to add a column for age to the database table used to store hero data. Change the type to Integer 16; we're going to calculate age as a whole number. That's all we have to do for now for the age attribute. Of course, as things stand, we can't do anything meaningful with this particular attribute, because it can't store anything, and we don't yet have any way to tell it how to calculate the age. That will change in a few minutes, when we create a custom subclass of NSManagedObject.

Adding the Favorite Color Attribute

Click the plus icon in the property pane again and select Add Attribute one more time. This time, call the new attribute favoriteColor and set the Type to Transformable. Once you've changed the Type pop-up to Transformable, you should notice a new field called Value Transformer Name: (Figure 6-6).

Making the favoriteColor attribute a transformable attribute

Figure 6.6. Making the favoriteColor attribute a transformable attribute

The Value Transformer Name: field is the key to using transformable attributes. We'll discuss value transformers in more depth in just a few minutes, but we'll populate this field now to save ourselves a trip back to the data model editor later. This field is where we need to put the name of the value transformer class that will be used to convert whatever object represents this attribute into an NSData instance for saving in the persistent store, and vice versa. The default value, NSKeyedUnarchiveFromData, will work with a great many objects by using NSKeyedArchiver and NSKeyedUnarchiver to convert any object that conforms to the NSCoding protocol into an instance of NSData. For most types of objects, this default transformer will work just fine, and our work would be basically done. Unfortunately, UIColor does not conform to NSCoding, which means that this value won't work for our situation. Instead, we need to write a custom value transformer class and provide its name here.

Because we have a crystal ball (well, OK, because we wrote the code), we know that we're going to call our value transformer UIColorRGBValueTransformer, so type that into the Value Transformer Name: field now.

Warning

The data model editor does not validate the Value Transformer Name: field to make sure it is a valid class. We're utilizing that fact right now to let us put in the name of a non-existent class that we'll write later. It's a double-edged sword, however, since mistyping the name of the value transformer won't show up as a problem until runtime and can be hard to debug, so make sure you are very careful about typing the correct name in this field.

Adding a Minimum Length to the Name Attribute

Next, let's add some validation to ensure that our name attribute is at least one character long. Single-click the name attribute to select it. In the Min. Length field, enter 1 to specify that the value entered into this field has to be at least one character long. This may seem like a redundant validation, since we already unchecked Optional in a previous chapter for this attribute, but the two do not do exactly the same thing. Because the Optional check box is unchecked, the user will be prevented from saving if name is nil. However, our application takes pains to ensure that name is never nil. For example, we give name a default value. If the user deletes that value, the text field will still return an empty string instead of nil. Therefore, to ensure that an actual name is entered, we're going to add this validation.

Save the data model.

Creating the Hero Class

It's now time to create our custom subclass of NSManagedObject. This will give us the flexibility to add custom validation and defaulting as well as the ability to use properties instead of key value coding, which will make our code easier to read and give us additional checks at compile time.

Single-click the Classes folder in the Groups & Files pane of Xcode. The data model editor should still be showing in the editing pane. If it's not, single-click the current version of the data model again, and then select the Classes folder. Now, single-click anywhere in the diagram pane. As you'll see in a moment, in order for our next step to work, the data model editor must be in the editing pane and the editing pane must be the active pane.

Now, select New File... from the File menu or press

Creating the Hero Class
Selecting the Managed Object Class template

Figure 6.7. Selecting the Managed Object Class template

Instead of prompting you for a file name, it's going to present you with a slightly different dialog than the one you usually see (Figure 6-8). This new dialog asks you only where it should put the generated file or files, but not what they should be called. It will name the subclasses automatically based on their entity name. Click the Next button again.

With the Managed Object Class template, you are not prompted for a name

Figure 6.8. With the Managed Object Class template, you are not prompted for a name

After clicking Next, you'll get a new dialog that lists all the entities in the active data model. In our case, we only have a single entity, so it's a pretty short list (Figure 6-9).

Make sure that the Hero entity is checked and that both Generate accessors and Generate Obj-C 2.0 Properties are checked. That will cause Xcode to create properties in the new class automatically for all the attributes. Leave the Generate validation methods check box unchecked. That option will generate method stubs for validating our attributes. Since we're going to write code to validate only one attribute, we'll write the methods by hand. If we were to select this, it would give us method stubs for validating every property in our entity. Once your screen looks like Figure 6-9, click the Finish button.

Selecting the entities for which to create custom classes

Figure 6.9. Selecting the entities for which to create custom classes

Tweaking the Hero Header

You should now have a pair of files called Hero.h and Hero.m in your Classes folder. Xcode also tweaked your data model so that the Hero entity uses this class rather than NSManagedObject at runtime. Single-click on the new Hero.h file now. It should look something look like this, though the exact order of your property declarations may not be exactly the same as ours:

#import <CoreData/CoreData.h>

@interface Hero :  NSManagedObject
{
}

@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * secretIdentity;
@property (nonatomic, retain) NSString * sex;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSDate * birthdate;
@property (nonatomic, retain) id favoriteColor;

@end

Warning

If your Hero.h file does not include declarations of age and favoriteColor, chances are you did not save properly somewhere along the way. If so, select Hero.h and Hero.m in your project file and press Delete, being sure the files are moved to the trash. Then go back, make sure your attributes were properly created in your data model, make sure the data model was saved, then recreate Hero.h and Hero.m.

We need to make two quick changes here. First, we want to make age read-only. We're not going to allow people to set a hero's age, we're just going to calculate it based on the birthdate. We also want to change favoriteColor from the generic id to UIColor to indicate that our favoriteColor attribute is, in fact, an instance of UIColor. This will give us some additional type safety by letting the compiler know what type of object represents the favoriteColor attribute. We also need to add a couple of constants that will be used in our validation methods. Make the following changes to Hero.h:

#import <CoreData/CoreData.h>

#define kHeroValidationDomain           @"com.Apress.SuperDB.HeroValidationDomain"
#define kHeroValidationBirthdateCode    1000

@interface Hero :  NSManagedObject
{
}

@property (nonatomic, retain) id favoriteColor;
@property (nonatomic, retain) UIColor * favoriteColor;
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, readonly) NSNumber * age;
@property (nonatomic, retain) NSString * secretIdentity;
@property (nonatomic, retain) NSString * sex;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSDate * birthdate;

@end

Don't worry too much about the two constants. We'll explain error domains and error codes in a few moments. Switch over to Hero.m. We've got a bit more work to do in the implementation file. Before we do that, let's talk about what we're going to do.

Defaulting

One of the most common Core Data tasks that requires you to subclass NSManagedObject is setting conditional default values for attributes, or setting the default value for attribute types that can't be set in the data model, such as default values for transformable attributes.

NSManagedObject has a method called awakeFromInsert that is specifically designed to be overridden by subclasses for the purpose of setting default values. It gets called immediately after a new instance of an object is inserted into a managed object context and before any code has a chance to make changes to or use the object.

In our case, we have a transformable attribute called favoriteColor that we want to default to white. To accomplish that, add the following method before the @end declaration in Hero.m:

- (void) awakeFromInsert {
    self.favoriteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
    [super awakeFromInsert];
}

Notice the use of the @dynamic keyword in Hero.m. This tells the compiler not to generate accessors and mutators for the property that follows. The idea here is that the accessors and mutators will be provided by the superclass at runtime. Don't worry too much about the specifics here, just know that this bit of complexity is required in order for Core Data to work properly.

Tip

Notice that we didn't use [UIColor whiteColor] for the default. The reason we used the colorWithRed:green:blue:alpha: factory method is because it always creates an RGBA color. UIColor supports several different color models. Later, we're going to be breaking the UIColor down into its separate components (one each for red, green, blue, and alpha) in order to save it in the persistent store. We're also going to be letting the user select a new color by manipulating sliders for each of these components. The whiteColor method, however, doesn't create a color using the RGBA color space. Instead, it creates a color using the grayscale color model, which represents colors with only two components, gray and alpha.

Simple enough. We just create a new instance of UIColor and assign it to favoriteColor. Another common usage of awakeFromInsert is for defaulting date attributes to the current date. We could, for example, default the birthdate attribute to the current date by adding the following line of code to awakeFromInsert:

self.birthdate = [NSDate date];

Validation

Core Data offers two mechanisms for doing attribute validation in code, one that's intended to be used for single-attribute validations, and one that's intended to be used when a validation depends on the value of more than one attribute. Single attribute validations are relatively straightforward. You might want to make sure that a date is valid, a field is not nil, or that a number attribute is not negative. Multi-field validations are a little more complex. Let's say that you had a Person entity, and it had a string attribute called legalGuardian where you keep track of the person who is legally responsible and able to make decisions for a person if they are a minor. You might want to make sure this attribute is populated, but you'd only want to do that for minors, not for adults. Multi-attribute validation would let you make the attribute required, if the person's age attribute is less than 18, but not otherwise.

Single-Attribute Validations

NSManagedObject provides a method for validating single attributes, called validateValue:forKey:error:. This method takes a value, key, and an NSError handle.

You could override this method and perform validation by returning YES or NO, based on whether the value is valid. If it doesn't pass, you would also be able to create an NSError instance to hold specific information about what is not valid and why.

You could do that. But don't. You never actually need to override this method because the default implementation uses a very cool mechanism to dynamically dispatch error handling to special validation methods that aren't defined in the class.

For example, let's say you have a field called, oh, say, birthdate. NSManagedObject will, during validation, automatically look for a method on our subclass called validateBirthdate:error:. It will do this for every attribute, so if you want to validate a single attribute, all you have to do is declare a method that follows the naming convention validateXxx:error: (where xxx is the name of the attribute to be validated), returning a BOOL that indicates whether the new value passed validation.

Let's use this mechanism to prevent the user from entering birthdates that occur in the future. Above the @end declaration in Hero.m, add the following method:

-(BOOL)validateBirthdate:(id *)ioValue error:(NSError **)outError{
    NSDate *date = *ioValue;
    if ([date compare:[NSDate date]] ==  NSOrderedDescending) {
        if (outError != NULL) {
            NSString *errorStr = NSLocalizedString(
                @"Birthdate cannot be in the future",
                @"Birthdate cannot be in the future");
            NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorStr
                forKey:NSLocalizedDescriptionKey];
            NSError *error = [[[NSError alloc] initWithDomain:kHeroValidationDomain
                code:kHeroValidationBirthdateCode
                userInfo:userInfoDict] autorelease];
            *outError = error;
        }
        return NO;
    }
    return YES;
}

Tip

Are you wondering why we're passed a pointer to a pointer to an NSError rather than just a pointer? Pointers to pointers allow a pointer to be passed by reference. In Objective-C methods, arguments, including object pointers, are passed by value, which means that the called method gets its own copy of the pointer that was passed in. So if the called method wants to change the pointer, as opposed to the data the pointer points to, we need another level of indirection. Thus, the pointer to the pointer.

As you can see from the preceding method, we return NO if the date is in the future, and YES if the date is in the past. If we return NO, we also take some additional steps. We create a dictionary, and store an error string under the key NSLocalizedDescriptionKey, which is a system constant that exists for this purpose. We then create a new instance of NSError and pass that newly created dictionary as the NSError's userInfo dictionary. This is the standard way to pass back information in validation methods and pretty much every other method that takes a handle to an NSError as an argument.

Notice that when we create the NSError instance, we use the two constants we defined earlier, kHeroValidationDomain and kHeroValidationBirthdateCode:

NSError *error = [[[NSError alloc] initWithDomain:kHeroValidationDomain
                code:kHeroValidationBirthdateCode
                userInfo:userInfoDict] autorelease];

Tip

Notice that we don't call super in the single-attribute validation methods. It's not that these methods are defined as abstract, it's that they simply don't exist. These methods are created dynamically at runtime, so not only is there no point in calling super, there's actually no method on super to call.

Every NSError requires an error domain and an error code. Error codes are integers that uniquely identify a specific type of error. An error domain defines the application or framework that generated the error. For example, there's an error domain called NSCocoaErrorDomain that identifies errors created by code in Apple's Cocoa frameworks. We defined our own error domain for our application using a reverse DNS-style string and assigned that to the constant kHeroValidationDomain. We'll use that domain for any error created as a result of validating the Hero object. We could also have chosen to create a single domain for the entire SuperDB application, but by being more specific, our application will be easier to debug.

By creating our own error domains, we can be as specific as we want to be. We also avoid the problem of searching through long lists of system-defined constants, looking for just the right code that covers a specific error. kHeroValidationBirthdateCode is the first code we've created in our domain, and we just picked the value 1000 for it arbitrarily. It would have been perfectly valid to choose 0, 1, 10000, or 34848 for this error code. It's our domain, we can do what we want.

Multiple-Attribute Validations

When you need to validate a managed object based on the values of multiple fields, the approach is a little different. After all the single-field validation methods have fired, another method will be called to let you do more complex validations. There are actually two such methods, one that is called when an object is first inserted into the context, and another when you save changes to an existing managed object.

When inserting a new managed object into a context, the multiple-attribute method you use is called validateForInsert:. When updating an existing object, the validation method you implement is called validateForUpdate:. In both cases, you return YES if the object passes validation, and NO if there's a problem. As with single-field validation, if you return NO, you should also create an NSError instance that identifies the specifics of the problem encountered.

In many instances, the validation you want to do at insert and at update are identical. In those cases, do not copy the code from one and paste it into the other. Instead, create a new validation method and have both validateForInsert: and validateForUpdate: call that new validation method.

In our application, we don't yet really have a need for any multiple-attribute validations, but let's say, hypothetically, that instead of making both name and secretIdentity required, we only wanted to require one of the two. We could accomplish that by making both name and secretIdentity optional in the data model, then using the multiple-attribute validation methods to enforce it. To do that, we would add the following three methods to our Hero class:

- (BOOL)validateNameOrSecretIdentity:(NSError **)outError {
    if ([self.name length] == 0) &&
         ([self.secretIdentity length] == 0)) {
        if (outError != NULL) {
            NSString *errorStr = NSLocalizedString(
                @"Must provide name or secret identity.",
                @"Must provide name or secret identity.");
NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorStr
                forKey:NSLocalizedDescriptionKey];
            NSError *error = [[[NSError alloc] initWithDomain:kHeroValidationDomain
                code:kHeroValidationNameOrSecretIdentityCode
                userInfo:userInfoDict] autorelease];
            *outError = error;
        }
    }
    return YES;
}

- (BOOL)validateForInsert:(NSError **)outError {
    return [self validateNameOrSecretIdentity:outError];
}

- (BOOL)validateForUpdate:(NSError **)outError {
    return [self validateNameOrSecretIdentity:outError];
}

Virtual Accessors

At the beginning of the chapter, we added a new attribute, called age, to our data model. We don't need to store the hero's age, however, because we can calculate it based on the hero's birthdate. Calculated attributes like this are often referred to as virtual accessors. They look like accessors, and as far as other objects are concerned, they can be treated just like the other attributes. The fact that we're calculating the value at runtime rather than retrieving it from the persistent store is simply an implementation detail.

As our Hero object stands right now, the age accessor will always return nil because we've told our data model not to create storage space for it in the persistent store and have made it read only. In order to make it behave correctly, we have to implement the logic to calculate age in a method that looks like an accessor (hence, the name "virtual accessor"). To do that, add the following method to Hero.m, just before @end:

- (NSNumber *)age {
    NSCalendar *gregorian = [[NSCalendar alloc]
        initWithCalendarIdentifier:NSGregorianCalendar];

    NSDateComponents *components = [gregorian
        components:NSYearCalendarUnit
        fromDate:self.birthdate
        toDate:[NSDate date]
        options:0];
    NSInteger years = [components year];

    [gregorian release];

    return [NSNumber numberWithInteger:years];
}

Now, any code that uses the age property accessor will be returned an NSNumber instance with the calculated age of the superhero.

Adding Validation Feedback

In Chapter 4, we created an abstract class named ManagedObjectAttributeEditor that encapsulates the common functionality shared by the various attribute editors. The ManagedObjectAttributeEditor class does not include code designed to save its managed object. We push that job to the subclasses, because we know that the actual mechanism for retrieving values from the user interface and putting them into an attribute is going to vary from subclass to subclass. But now, we want to add validation feedback when the edited attribute fails validation, and we don't want to duplicate the same functionality in each subclass's save method.

If you look at the subclasses of ManagedObjectAttributeEditor, you'll notice that they all share a bit of logic at the end of their save methods:

NSError *error;
    if (![managedObject.managedObjectContext save:&error])
        NSLog(@"Error saving: %@", [error localizedDescription]);

    [self.navigationController popViewControllerAnimated:YES];

Every attribute editor has to save the managed object after updating it with the newly edited value. At this point in the save method, we'll find out about any validation error, so this is where we need to add the code to notify the user of those errors. Let's refactor this shared functionality into ManagedObjectAttributeEditor.

Now, we could put this code in the save method of ManagedObjectAttributeEditor and have each of the subclasses call super after copying the data from their user interface to the managed object. However, it's still important to make sure that subclasses do actually implement save. If we have the subclasses call super, then we've got no place to place an exception. Instead, we'll leave the exception in ManagedObjectAttributeEditor's save method. If a subclass does not implement save, ManagedObjectAttributeEditor's save will be called and this exception will be thrown.

To complement this strategy, we'll create a new method on ManagedObjectAttributeEditor called validateAndPop that will attempt to save the managed object. If the object passes validation, it will pop the controller off the navigation stack, returning the user to the previous level in the nagivation hierarchy. If validation fails, however, we will present an alert telling the user what went wrong. We'll present them with the option of fixing it, or canceling their changes and reverting to the previous value.

Updating the ManagedObjectAttributeEditor Header File

Single-click on ManagedObjectAttributeEditor.h. We need to make two changes to this file. First, we need to conform the class to UIAlertViewDelegate. We're going to be using an alert view to notify the user if validation failed, and we need to conform to this protocol to find out whether the user chose to fix the problem or to cancel the change. We also need to add a declaration for the new validateAndPop: method. Here are the necessary changes:

#import <UIKit/UIKit.h>
#define kNonEditableTextColor    [UIColor colorWithRed:.318 green:0.4 blue:.569 alpha:1.0]

@interface ManagedObjectAttributeEditor : UITableViewController
    <UIAlertViewDelegate> {
    NSManagedObject         *managedObject;
    NSString                *keypath;
    NSString                *labelString;

}

@property (nonatomic, retain) NSManagedObject *managedObject;
@property (nonatomic, retain) NSString *keypath;
@property (nonatomic, retain) NSString *labelString;

-(IBAction)cancel;
-(IBAction)save;
-(IBAction)validateAndPop;

@end

Save ManagedObjectAttributeEditor.h and switch to ManagedObjectAttributeEditor.m.

Updating the ManagedObjectAttributeEditor Implementation File

Add the following method to the ManagedObjectAttributeEditor implementation, somewhere between the @implementation and @end tags. We put it right after the save method, but feel free to put it anywhere that makes sense to you as long as it's within the class implementation. It's your code, after all, and you're the one who may need to find this method again.

-(IBAction)validateAndPop {
    NSError *error;
    if (![managedObject.managedObjectContext save:&error]) {

        NSString *message = nil;
        if ([[error domain] isEqualToString:@"NSCocoaErrorDomain"]) {
            NSDictionary *userInfo = [error userInfo];
            message = [NSString stringWithFormat:NSLocalizedString(
                @"Validation error on %@
Failed condition: %@",
                @"Validation error on %@, (failed condition: %@)"),
                [userInfo valueForKey:@"NSValidationErrorKey"],
                [userInfo valueForKey:@"NSValidationErrorPredicate"]];
        }
        else
            message = [error localizedDescription];

        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
            NSLocalizedString(@"Validation Error", @"Validation Error")
            message:message
            delegate:self
            cancelButtonTitle:NSLocalizedString(@"Cancel", @"Cancel")
otherButtonTitles:NSLocalizedString(@"Fix", @"Fix"), nil];
        [alert show];
        [alert release];
    }
    else
        [self.navigationController popViewControllerAnimated:YES];
}

There's nothing really new here. We attempt to save and, if the attempt fails, we pull the information out of the returned NSError object. The only unusual thing here is that we retrieve the information a little bit differently if the error came from our application than if was the result of a validation error generated by the data model. Either way, we present an alert with two buttons, one to cancel the changes, and the other to stay in the editor and make changes to fix the problem.

Next, we need to add our alert view delegate method, which will get called when the user presses one of the two buttons on the alert view. Add the following code to your class implementation also. We like to put the delegate methods at the end of the file, right before the @end statement.

#pragma mark -
#pragma mark Alert View Delegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == [alertView cancelButtonIndex]) {
        [self.managedObject.managedObjectContext rollback];
        [self.navigationController popViewControllerAnimated:YES];
    }
}

If the user pressed the Cancel button, we roll back the managed object context, which returns the context back to the state it was in when it was last saved. If we didn't do this, then the change the user made would still be in memory, it just wouldn't have been saved to the persistent store, and that would cause problems with any future saves. It would also simply be wrong, because the user would see the unsaved, changed value in the user interface, even though they just did a cancel.

After restoring the hero in memory to its last saved state, our controller then pops itself off the stack, which returns the user to the previous view. In our case, the previous view is the hero editing view.

Updating the Subclasses to Use Validation

We currently have three subclasses of ManagedObjectAttributeEditor, and all three of them currently handle saving and popping themselves off the stack themselves. We need to modify the save method of all three classes to use the new functionality in their superclass instead.

Updating ManagedObjectStringEditor

Single-click ManagedObjectStringEditor.m and look for the save method. Remove the existing code to save and pop the controller off the navigation stack and replace it with a call to the superclass's validateAndPop method. When you're done, the save method should look like this:

-(IBAction)save {
    NSUInteger onlyRow[] = {0, 0};
    NSIndexPath *onlyRowPath = [NSIndexPath indexPathWithIndexes:onlyRow length:2];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:onlyRowPath];
    UITextField *textField = (UITextField *)[cell.contentView
                                             viewWithTag:kTextFieldTag];
    [self.managedObject setValue:textField.text forKey:self.keypath];

    [self validateAndPop];
}

Save ManagedObjectStringEditor.m.

Updating ManagedObjectDateEditor

Next, single-click ManagedObjectDateEditor.m and do the same thing. When you're done, it should look like this:

-(IBAction)save {
    [self.managedObject setValue:self.datePicker.date forKey:self.keypath];
    [self validateAndPop];
}

Save ManagedObjectDateEditor.m.

Updating ManagedObjectSingleSelectionListEditor

Finally, single-click ManagedObjectSingleSelectionListEditor.m and repeat the process one more time. When you're done, the save method should look like this:

-(IBAction)save {
    UITableViewCell *selectedCell = [self.tableView
        cellForRowAtIndexPath:lastIndexPath];
    NSString *newValue = selectedCell.textLabel.text;
    [self.managedObject setValue:newValue forKey:self.keypath];
    [self validateAndPop];
}

Save ManagedObjectSingleSelectionListEditor.m.

Creating the Value Transformer

Earlier in the chapter, we added an attribute called favoriteColor, and set its type to Transformable. As we stated then, often you'll be able to leave the transformable attribute's transformer class at NSKeyedUnarchiveFromData and be completely done with the process, since that provided class will use an NSKeyedArchiver to convert an object instance into an NSData object that can be stored in the persistent store, and an NSKeyedUnarchiver to take the NSData object from the persistent store and reconstitute it back into an object instance.

In the case of UIColor, we can't do that, because UIColor doesn't conform to NSCoding and can't be archived using an NSKeyedArchiver. As a result, we have to manually write a value transformer to handle the transformation.

Writing a value transformer is actually quite easy. We start by subclassing the NSValueTransformer class. We then override transformedValueClass, which is a method that returns the class of objects that this transformer can convert. Our value transformer will return an instance of the class UIColor because that's the type of attribute we want to store. Transformable Core Data attributes have to be able to both convert from an instance of UIColor to an instance of NSData and back from an instance of NSData to an instance of UIColor. Otherwise, we wouldn't be able to both save and retrieve values from the persistent store. As a result, we also need to override a method called allowsReverseTransformation, returning YES to indicate that our converter supports two-way conversions.

After that, we override two methods. One, transformedValue:, takes an instance of the class we want to convert and returns the converted object. For transformable Core Data attributes, this method will take an instance of the attribute's underlying class and will return an instance of NSData. The other method we have to implement, reverseTransformedValue:, takes a converted object instance and reconstitutes the original object. For a Core Data transformable attribute, that means taking an instance of NSData and returning an object that represents this attribute. Let's do it.

Single-click the Classes folder in the Groups & Files pane and create a new file. Xcode doesn't provide a file template for value transformers, so select the Objective-C class template and create a subclass of NSObject and name it UIColorRGBValueTransformer.m.

Tip

Some of the class names we're creating may seem unnecessarily long, but it's important that class names be descriptive. UIColor supports many color models but, for our needs, we only need to convert RGBA colors, because we're only going to allow the user to create RGBA colors. It's important to indicate this limitation in the class name because at some point in the future we may need a UIColor value transformer that supports all color models. When we revisit this code in the future, we'll have a built-in reminder that this class only handles one of the possible color models that UIColor supports.

Single-click UIColorRGBValueTransformer.h and change the superclass from NSObject to NSValueTransformer.

In addition, since UIColor is part of UIKit, not Foundation, change the line that currently reads:

#import <Foundation/Foundation.h>

to read:

#import <UIKit/UIKit.h>

Once you've made those two changes, save the file and switch over to UIColorRGBValueTransformer.m.

Now, we have to implement the four methods that will allow our value transformer class to convert instances of UIColor to NSData and vice versa. Add the following four methods to your class:

#import "UIColorRGBValueTransformer.h"


@implementation UIColorRGBValueTransformer
+ (Class)transformedValueClass {
    return [NSData class];
}

+ (BOOL)allowsReverseTransformation {
    return YES;
}

// Takes a UIColor, returns an NSData
- (id)transformedValue:(id)value {
    UIColor *color = value;
    const CGFloat *components = CGColorGetComponents(color.CGColor);
    NSString *colorAsString = [NSString stringWithFormat:@"%f,%f,%f,%f",
        components[0], components[1], components[2], components[3]];
    return [colorAsString dataUsingEncoding:NSUTF8StringEncoding];
}

// Takes an NSData, returns a UIColor
- (id)reverseTransformedValue:(id)value {
    NSString *colorAsString = [[[NSString alloc] initWithData:value
        encoding:NSUTF8StringEncoding] autorelease];
    NSArray *components = [colorAsString componentsSeparatedByString:@","];
    CGFloat r = [[components objectAtIndex:0] floatValue];
    CGFloat g = [[components objectAtIndex:1] floatValue];
    CGFloat b = [[components objectAtIndex:2] floatValue];
    CGFloat a = [[components objectAtIndex:3] floatValue];
    return [UIColor colorWithRed:r green:g blue:b alpha:a];
}

@end

There are many approaches we could have used to convert a UIColor instance into an NSData instance. We opted for a relatively simple one here. We store the color's four component values in a string with commas between the values. Since we're only dealing with RGBA colors, we know we will always and only have four components, so we're able to simplify the transformation greatly. Now we have a way to store colors in Core Data, so let's create a way for the user to enter a color.

Creating the Color Attribute Editor

Single-click the Classes folder in Xcode's Groups & Files pane and select New File... from the File menu. When prompted, select Objective-C Class from the Cocoa Touch Class category and make sure the Subclass of pop-up is set to NSObject. When prompted for a name, type ManagedObjectColorEditor.m and make sure that Also create "ManagedObjectColorEditor.h" is checked. Once the files are created, single-click ManagedObjectColorEditor.h and replace the existing contents with the following:

#import <UIKit/UIKit.h>
#import "ManagedObjectAttributeEditor.h"

#define kNumberOfSections           2
#define kNumberOfRowsInSection0     1
#define kSliderTag                  5000
#define kColorViewTag               5001

enum colorSliders {
    kRedRow = 0,
    kGreenRow,
    kBlueRow,
    kAlphaRow,
    kNumberOfColorRows
};

@interface ManagedObjectColorEditor : ManagedObjectAttributeEditor {
    UIColor *color;
}

@property (nonatomic, retain) UIColor *color;
- (IBAction)sliderChanged;
@end

If you look back at Figure 6-2, you can see that our color editor is going to consist of a table with two sections. The first section will have a single row that will display the currently selected color. The second section will have four rows with sliders, one for each of the four components of an RGBA color. The first two constants and the enum will be used to make our code more legible when referring to section and rows. kSliderTag and kColorViewTag will be used as tags on the slider and color views to make them easier to retrieve from the cell they're on, just as we did in Chapter 8 of Beginning iPhone 3 Development (Apress, 2009).

We've subclassed ManagedObjectAttributeEditor once again, so we inherit the keypath, labelString, and managedObject properties, but we do need to add a property to hold the color as it's being edited. We also create an action method that the four sliders can call when they've changed so that we can update the interface and show the new colors indicated by the sliders. Save ManagedObjectColorEditor.h and switch over to the implementation file. Replace the existing contents of that file with the following code to implement the color attribute editor:

#import "ManagedObjectColorEditor.h"

@implementation ManagedObjectColorEditor
@synthesize color;
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.color = [self.managedObject valueForKey:self.keypath];
}

- (IBAction)sliderChanged {

    CGFloat components[4];
    for (int i = 0; i < kNumberOfColorRows; i++) {
        NSUInteger indices[] = {1, i};
        NSIndexPath *indexPath = [NSIndexPath indexPathWithIndexes:indices
           length:2];
        UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
        UISlider *slider = (UISlider *)[cell viewWithTag:kSliderTag];
        components[i] = slider.value;
    }
    self.color = [UIColor colorWithRed:components[0] green:components[1]
        blue:components[2] alpha:components[3]];

    NSUInteger indices[] = {0,0};
    NSIndexPath *indexPath = [NSIndexPath indexPathWithIndexes:indices length:2];
    UITableViewCell *colorCell = [self.tableView cellForRowAtIndexPath:indexPath];
    UIView *colorView = [colorCell viewWithTag:kColorViewTag];
    colorView.backgroundColor = self.color;
}

-(IBAction)save {
    [self.managedObject setValue:self.color forKey:self.keypath];
    [self validateAndPop];
}

- (void)dealloc {
    [color release];
    [super dealloc];
}

#pragma mark -
#pragma mark Table View Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return kNumberOfSections;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (section == 0)
        return kNumberOfRowsInSection0;
    else
        return kNumberOfColorRows;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {

    static NSString *GenericManagedObjectColorEditorColorCell =
        @"GenericManagedObjectColorEditorColorCell";
    static NSString *GenericManagedObjectColorEditorSliderCell =
@"GenericManagedObjectColorEditorSliderCell";

    NSString *cellIdentifier = nil;

    NSUInteger row = [indexPath row];
    NSUInteger section = [indexPath section];
    if (section == 0)
        cellIdentifier = GenericManagedObjectColorEditorColorCell;
    else
        cellIdentifier = GenericManagedObjectColorEditorSliderCell;


    UITableViewCell *cell = [tableView
        dequeueReusableCellWithIdentifier:cellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:cellIdentifier] autorelease];

        UIView *contentView = cell.contentView;

        if (section == 0){
            UIView *colorView = [[UIView alloc] initWithFrame:
                CGRectMake(5.0, 5.0, 290.0, 33.0)];
            colorView.backgroundColor = self.color;
            colorView.tag = kColorViewTag;
            [contentView addSubview:colorView];
        }
        else {

            if (color == nil)
            self.color = [UIColor colorWithRed:1.0 green:1.0 blue:1.0
                alpha:1.0];

            components = CGColorGetComponents(color.CGColor);

           UISlider * slider = [[UISlider alloc] initWithFrame:
                CGRectMake(70.0, 10.0, 210.0, 20.0)];
            slider.tag = kSliderTag;
            slider.maximumValue = 1.0;
            slider.minimumValue = 0.0;
            slider.value = components[row];
            [slider addTarget:self action:@selector(sliderChanged)
                forControlEvents:UIControlEventValueChanged];
            UILabel *label = [[UILabel alloc] initWithFrame:
                CGRectMake(20.0, 10.0, 50.0, 20.0)];
            switch (row) {
                case kRedRow:
                    label.text = NSLocalizedString(@"R",
                        @"R (short for red)");
                    label.textColor = [UIColor redColor];
                    break;
                case kGreenRow:
                    label.text = NSLocalizedString(@"G",
                        @"G (short for green)");
                    label.textColor = [UIColor greenColor];
                    break;
case kBlueRow:
                    label.text = NSLocalizedString(@"B",
                        @"B (short for blue)");
                    label.textColor = [UIColor blueColor];
                    break;
                case kAlphaRow:
                    label.text = NSLocalizedString(@"A",
                        @"A (short for alpha)");
                    label.textColor = [UIColor colorWithRed:0.0
                        green:0.0 blue:0.0 alpha:0.5];
                    break;
                default:
                    break;
            }
            [contentView addSubview:slider];
            [contentView addSubview:label];

            [slider release];
            [label release];
        }

    }
    return cell;
}

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    return nil;
}

@end

There's nothing really new there. Look over the code and make sure you know what it's doing, but there's nothing there that should really need explanation.

Displaying the New Attributes in Hero Edit Controller

We've added two new attributes to our data model, but we haven't added them to our user interface yet. Remember from Chapter 4 that the attributes displayed by HeroEditController are controlled by those paired, nested arrays we create in viewDidLoad. Until we add rows to those arrays to represent the new attributes, they won't show up or be editable. Single-click HeroEditController.m and replace viewDidLoad: with this new version that adds rows to each of the paired, nested arrays for the calculated attribute age and the transformable attribute favoriteColor.

- (void)viewDidLoad {
    sectionNames = [[NSArray alloc] initWithObjects:
        [NSNull null],
        NSLocalizedString(@"General", @"General"),
        nil];

   rowLabels = [[NSArray alloc] initWithObjects:

        // Section 1
[NSArray arrayWithObject:NSLocalizedString(@"Name", @"Name")],

        // Section 2
        [NSArray arrayWithObjects:
            NSLocalizedString(@"Identity", @"Identity"),
            NSLocalizedString(@"Birthdate", @"Birthdate"),
            NSLocalizedString(@"Age", @"Age"),
            NSLocalizedString(@"Sex", @"Sex"),
            NSLocalizedString(@"Fav. Color", @"Favorite Color"),
            nil],

         // Sentinel
         nil];

    rowKeys = [[NSArray alloc] initWithObjects:

        // Section 1
        [NSArray arrayWithObjects:@"name", nil],

        // Section 2
        [NSArray arrayWithObjects:@"secretIdentity", @"birthdate",
            @"age", @"sex", @"favoriteColor", nil],

        // Sentinel
        nil];

    rowControllers = [[NSArray alloc] initWithObjects:

        // Section 1
        [NSArray arrayWithObject:@"ManagedObjectStringEditor"],

        // Section 2
        [NSArray arrayWithObjects:@"ManagedObjectStringEditor",
            @"ManagedObjectDateEditor",
            [NSNull null],
            @"ManagedObjectSingleSelectionListEditor",
            @"ManagedObjectColorEditor",
            nil],

        // Sentinel
        nil];

    rowArguments = [[NSArray alloc] initWithObjects:

        // Section 1
        [NSArray arrayWithObject:[NSNull null]],

        // Section 2,
        [NSArray arrayWithObjects:[NSNull null],
            [NSNull null],
            [NSNull null],
            [NSDictionary dictionaryWithObject:[NSArray
                arrayWithObjects:@"Male", @"Female", nil]
                forKey:@"list"],
            [NSNull null],
            [NSNull null],
            nil],
// Sentinel
        nil];

    [super viewDidLoad];
}

Notice that in rowControllers, for the age row, we've used our good old friend NSNull. We're using that to indicate that there is no controller class for that row. The user can't drill down to edit this value. In other words, it's read only.

The Display Problem

If you build and run your application, you'll run into a subtle problem. Here's a hint. It has something to do with the display of UIColor. Can you guess what it is?

The problem is that UIColor doesn't respond to the heroValueDisplay method. We could create a category to add that method to UIColor, but the real problem is this: how do we meaningfully represent a color using an instance of NSString, the type returned by heroValueDisplay? We could create a string that displays the four components of the color, but to most end users, those numbers are meaningless. Our users are going to expect to see the actual color when they're viewing the hero, and we don't have any mechanism right now for showing colors on a row.

The question at this point is, do we go back and re-architect our application so that it can support the display of a UIColor on a table view row? We could resolve this issue, for example, by changing the heroValueDisplay protocol and methods that currently return an NSString instance and have them return a UIView instance, where the UIView contains everything that we want to display in a particular row. That's a good idea, but it will require some relatively extensive changes in many different places in our application's code.

Bottom line, we need to figure out if it makes sense to do major renovations to our code to accommodate this need. Is this a one time thing, or do we need do some fairly intrusive refactoring to create a more general solution? We don't want to over-engineer. We don't want to have to do complex changes to multiple classes to support functionality that we'll never need outside of this single instance.

There isn't really One Right Answerâ„¢ here. For the sake of argument, we're going to say that we don't foresee needing the ability to display a color anywhere else in our application. Then the question becomes whether there is a less intrusive way of handling this that's not going to make our code significantly harder to maintain. In this situation, there is, and we're going to use it. We can implement the functionality we need by conforming UIColor to the HeroValueDisplay protocol and then adding just two lines of code to HeroEditController.

Single-click HeroValueDisplay.h (it's in the Categories group) and add the following category declaration at the bottom of the file:

@interface UIColor (HeroValueDisplay) <HeroValueDisplay>
- (NSString *)heroValueDisplay;
@end

Save HeroValueDisplay.h and switch over to HeroValueDisplay.m to write the implementation of the heroValueDisplay method for UIColor. Add the following at the end of the file:

@implementation UIColor (HeroValueDisplay)
- (NSString *)heroValueDisplay {
    return [NSString stringWithFormat:@"%C%C%C%C%C%C%C%C%C%C",0x2588, 0x2588,
        0x2588, 0x2588, 0x2588, 0x2588, 0x2588, 0x2588, 0x2588, 0x2588];
}
@end

This is probably non-obvious, so we'll explain. What we're doing here is creating an NSString instance that contains a sequence of Unicode characters. The 0x2588 character is the Unicode full block character, which is a solid rectangle that takes up the full space of the glyph. If you place several full blocks together in a string, they appear as a rectangle like the one you see in the bottom row of Figure 6-1. Now, we just need to make that rectangle display in color.

Single-click HeroEditController.m and add the following two lines of code to tableView:cellForRowAtIndexPath:.

- (UITableViewCell *)tableView:(UITableView *)tableView
  cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Hero Edit Cell Identifier";

    UITableViewCell *cell = [tableView
                             dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2
                                       reuseIdentifier:CellIdentifier] autorelease];
    }

    NSString *rowKey = [rowKeys nestedObjectAtIndexPath:indexPath];
    NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];

    id <HeroValueDisplay, NSObject> rowValue = [hero valueForKey:rowKey];

    cell.detailTextLabel.text = [rowValue heroValueDisplay];
    cell.textLabel.text = rowLabel;
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    if ([rowValue isKindOfClass:[UIColor class]])
        cell.detailTextLabel.textColor = (UIColor *)rowValue;

    return cell;
}

The two lines of code we just added look at the underlying class of the attribute we're displaying, and if it's UIColor, or a subclass of UIColor, then we set the text label's textColor property to the value stored in the hero's favoriteColor attribute. This will cause that string of Unicode full blocks to be drawn in that color. Compile and run the application, and the two new attributes should be there (Figure 6-10).

Almost there. The new values are being displayed and the favorite color attribute can be edited.

Figure 6.10. Almost there. The new values are being displayed and the favorite color attribute can be edited.

This is almost done. There's just one little detail we need to take care of. Look at the Age row. Something's not right there. Age is calculated and can't be edited by the user. Yet there's a disclosure indicator on the row, which tells us as a user that we can tap it to edit it. Go ahead and tap it if you want. We'll wait. After it crashes, come on back and we can chat about how to fix it.

Adding View-Only Support to Hero Edit Controller

We need to do two things here. First, we need to get rid of the disclosure indicator so the user doesn't think they can drill down into that attribute to edit it. Then, we need to change the code so that even if a user does tap that row, nothing bad happens. You know, this is actually a pretty good task for you to try on your own if you want. Give it a try. We'll wait right here.

Hiding the Disclosure Indicator

In HeroEditController.m, find the method tableView:cellForRowAtIndexPath: and replace this line of code:

cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

with these lines of code:

id rowController = [rowControllers
nestedObjectAtIndexPath:indexPath];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    cell.accessoryType = (rowController == [NSNull null]) ?
        UITableViewCellAccessoryNone :
        UITableViewCellAccessoryDisclosureIndicator;
    if ([rowValue isKindOfClass:[UIColor class]])
        cell.detailTextLabel.textColor = (UIColor *)rowValue;

Previously, we were just setting every row to use the disclosure indicator. Now, instead, we retrieve that singleton instance of NSNull and the name of the class that is responsible for editing this type of attribute. If that controller class is NSNull, it means there is no controller class to drill down into. If there's no controller class, then we set the accessory type to UITableViewCellAccessoryNone, which means there will be nothing in the accessory view of this row. If there is a controller class to drill down into, we set the accessory view to show the disclosure indicator, just like we were previously doing. Simple enough, right? Let's take care of the other half of the equation.

Handling Taps on Read-Only Attributes

As you may remember from Beginning iPhone 3 Development, table view delegates have a way of disallowing a tap on a specific row. If we implement the method tableView:willSelectRowAtIndexPath: and return nil, the row won't get selected. Add the following method to HeroEditController.m, down in the table view portion of the code:

- (NSIndexPath *)tableView:(UITableView *)tableView
        willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    id controllerClassName = [rowControllers nestedObjectAtIndexPath:indexPath];
    return (controllerClassName == (id)[NSNull null]) ? nil : indexPath;
}

In this method, we retrieve the controller class for the tapped row. If we get an instance of NSNull back, we return nil to indicate that the user cannot select this row. If we retrieve any other value, we return indexPath, which allows the selection to continue.

By disallowing the selection when the row has no controller, the code in tableView:didSelectRowAtIndexPath: will never get called when a read-only row is tapped. As a result, we don't have to make any changes to that method, so we're ready to go. Build and run your project and play around with SuperDB some more. The editing view should now look like Figure 6-1. If you tap the Fav. Color row, it should drill down to something that looks like Figure 6-2. If you tap on the Age row, it should do nothing. If you try to enter an invalid value into any attribute, you should get an alert and be given the opportunity to fix or cancel the changes you made. And all is right with the world. Well, at least with our app. For now.

Color Us Gone

By now, you should have a good grasp on just how much power you gain from subclassing NSManagedObject. You've seen how to use it to do conditional defaulting and both single-field and multi-field validation. You also saw how to use custom managed objects to create virtual accessors.

You saw how to politely inform your user when they've entered an invalid attribute that causes a managed object to fail validation, and you saw how to use transformable attributes and value transformers to store custom objects in Core Data.

This was a dense chapter, but you should really be starting to get a feel for just how flexible and powerful Core Data can be. We've got one more chapter on Core Data before we move on to other parts of the iPhone 3 SDK. When you're ready, turn the page to learn about relationships and fetched properties.

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

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