Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 57 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
57
Dung lượng
0,92 MB
Nội dung
CHAPTER 6: Custom Managed Objects 155 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. CHAPTER 6: Custom Managed Objects 156 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> CHAPTER 6: Custom Managed Objects 157 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. CHAPTER 6: Custom Managed Objects 158 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 CHAPTER 6: Custom Managed Objects 159 @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 = CHAPTER 6: Custom Managed Objects 160 @"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; CHAPTER 6: Custom Managed Objects 161 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 CHAPTER 6: Custom Managed Objects 162 [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], CHAPTER 6: Custom Managed Objects 163 // 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; CHAPTER 6: Custom Managed Objects 164 @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). [...]... 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... 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 167 168 CHAPTER 6: Custom Managed Objects 169 Chapter 7 Relationships, Fetched... powers is actually an instance of the same object used to edit heroes When users drill down into one of the reports, they will get a list of the other heroes that meet the selected criteria (Figure 7 3) Figure 7 3 The Reports section on our hero will let us find other heroes who meet certain criteria in relation to the hero we’re currently editing Here, for example, we’re seeing all the heroes who were born... set those properties The property that represents a to-one relationship is an instance of NSManagedObject or a subclass of NSManagedObject, so setting the address looks just like setting attributes: 1 73 1 74 CHAPTER 7: Relationships, Fetched Properties, and Expressions NSManagedObject *address = [NSEntityDescription insertNewObjectForEntityForName: @"Address" inManagedObjectContext:thePerson.managedObjectContext];... relationship later CHAPTER 7: Relationships, Fetched Properties, and Expressions NOTE: You can read more about how the absence of inverse relationships can cause integrity problems here: http://developer.apple.com/mac/library/documentation/Cocoa/Concept ual/CoreData/Articles/cdRelationships.html#//apple_ref/doc/uid/TP4 0001857–SW6 Delete Rules Every relationship, regardless of its type, has something called... editor Using the plus icon in the lower-left corner of the data model editor’s entity pane, add a new entity and call it Power You can leave all the other fields at their default values (Figure 7 4) Figure 7 4 Rename the new entity Power and leave the other fields at their default values If you look back at Figure 7–2, you can see that our Power object has two fields: one for the name of the power and... property is being called Your first instinct might be to specify Key for the right operand, since we want to compare to the birthdate attribute on this object However, it doesn’t work that way Key 1 83 1 84 CHAPTER 7: Relationships, Fetched Properties, and Expressions operators always and only refer to keys on the managed objects being retrieved—no matter on which side of the equation they appear So,... expression, your predicate builder sheet should look like Figure 7– 13 NOTE: The syntax for expressions is documented in the Predicates Programming Guide: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Concept ual/Predicates/predicates.html 185 186 CHAPTER 7: Relationships, Fetched Properties, and Expressions Figure 7– 13 You can also enter criteria as expressions Hit the OK button, and this... this predicate You could also have accomplished this by clicking the + button, but we wanted you to see this other way After you add the second criteria, your predicate should look like Figure 7– 14 Figure 7– 14 The predicate builder allows you to build complex criteria using Boolean logic Here, we have two criteria being joined by an AND operator Because we selected AND, this fetched property will return... these changes, we need to talk about a few concepts, and then make some changes to our data model Relationships We introduced the concept of Core Data relationships back in Chapter 2 Now we will go into more detail, and see how these can be used in applications The relationship is one of the most important concepts in Core Data Without relationships, entities would be isolated There would be no way to . 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. 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. 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