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