Now switch to your CMColorView.m implementation file. You’re going to add a -drawRect: method that draws a 2D hue/saturation color chart at the current brightness level. At the position within the color chart that representscurrent hue/saturation, the view draws a circle filled with that color.
It’s a fair amount of code, and it’s not the focus of this chapter, so I’ll gloss over the details. The code you need to add to CMColorView.m is in Listings 8-1 and 8-2. If you’re writing this app as you work through this chapter, Iapplaud you. I will, however, suggest that you save yourself a lot of typing and copy the code for the -dealloc and -drawRect: methods from the CMColorView.m file that you’ll find
in the Learn iOS Development Projects ➤ Ch 8 ➤ ColorModel-4 ➤ ColorModel folder.
Listing 8-1. CMColorView.m private @interface
#define kCircleRadius 40.0f
@interface CMColorView ()
{
}
@end
CGImageRef hsImageRef; float brightnes;
Listing 8-2. CMColorView.m -dealloc and -drawRect: methods - (void)dealloc
{
if (hsImageRef!=NULL) CGImageRelease(hsImageRef);
}
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
CGContextRef context = UIGraphicsGetCurrentContext();
if (hsImageRef!=NULL &&
( brightness!=_colorModel.brightness || bounds.size.width!=CGImageGetWidth(hsImageRef) || bounds.size.height!=CGImageGetHeight(hsImageRef) ) )
{ CGImageRelease(hsImageRef); hsImageRef = NULL;
}
if (hsImageRef==NULL)
{
brightness = _colorModel.brightness; NSUInteger width = bounds.size.width; NSUInteger height = bounds.size.height; typedef struct {
uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha;
} Pixel;
NSMutableData *bitmapData =
[NSMutableData dataWithLength:sizeof(Pixel)*width*height];
for ( NSUInteger y=0; y<height; y++ )
{
for ( NSUInteger x=0; x<width; x++ )
{
UIColor *color = [UIColor colorWithHue:(float)x/(float)width saturation:1.0f-(float)y/(float)height brightness:brightness/100
alpha:1]; float red,green,blue,alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha]; Pixel *pixel = ((Pixel*)bitmapData.bytes)+x+y*width;
pixel->red = red*255; pixel->green = green*255; pixel->blue = blue*255; pixel->alpha = 255;
}
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGDataProviderRef provider = CGDataProviderCreateWithCFData(
( bridge CFDataRef)bitmapData); hsImageRef = CGImageCreate(width,height,8,32,width*4,
colorSpace,kCGBitmapByteOrderDefault, provider,NULL,false, kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace); CGDataProviderRelease(provider);
}
CGContextDrawImage(context,bounds,hsImageRef); CGRect circleRect = CGRectMake(
bounds.origin.x+bounds.size.width*_colorModel.hue/360-kCircleRadius/2, bounds.origin.y+bounds.size.height*_colorModel.saturation/100-kCircleRadius/2, kCircleRadius,
kCircleRadius);
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:circleRect]; [_colorModel.color setFill];
[circle fill]; circle.lineWidth = 3;
[[UIColor blackColor] setStroke]; [circle stroke];
}
In a nutshell, the CMColorView draws a two-dimensional graph of the possible hue/saturation combinations at the current brightness level. (When iOS devices come out with 3D displays, you can revise this code to draw a3D image instead!) I’ll refer back to this code in Chapter 11, where I explain various drawing techniques.
The point of interest (for this chapter) is that the CMColorValue has a direct reference to the CMColor data model object, so your controller doesn’t have to explicitly update it with a new color value anymore. All your controllerneeds to do is tell the CMColorView object when it needs to redraw; the CMColorView will use the data model directly to obtain whatever information it needed to draw itself.
For this to happen, your controller needs to establish this connection when it creates the data model and view objects. In your CMViewController.m file, find the -viewDidLoad method and add this one bold line:
- (void)viewDidLoad
{
[super viewDidLoad]; self.colorModel = [CMColor new]; self.colorView.colorModel = self.colorModel;
}
When the view objects are created (when the Interface Builder file loads), the controller creates the data model object and connects it to the colorView object.
Now replace the code that you used to set the color to draw (via colorView’s backgroundColor
property) with code that simply tells the colorView object that it needs to redraw itself, shown in bold:
- (void)updateColor
{
[self.colorView setNeedsDisplay];
Run your new app and try it out. This is a dramatically more interesting interface, as shown in Figure 8-25.
Figure 8-25. ColorModel with CMColorView
This version of your app represents the next level of MVC sophistication. Instead of spoon-feeding simple values to your view objects, you now have a view object that understands your data model and obtains the values it needsdirectly. But the controller still has to remember to refresh all of the views whenever it changes the data model. Let’s take a different approach, and have the data model tell the controller when it changes.
Being a K-V Observer
Way back in the “MVC Communications” section, I described a simple arrangement where the data model sent notifications to view objects (see Figure 8-1) letting them know when they need to update their display.You’ve already done this in your MyStuff app. You added a
-postDidChangeNotification method to your MyWhatsit class. That method notified any interested parties that an item in your data model had changed. Your table view observed those notifications and redrew itself as needed.
Using NSNotificationCenter to communicate data model changes to views is a perfect example of MVC communications. Save that in your bag of “iOS solutions I know.” I won’t repeat that solution here. Instead, I’m going toshow you an even more sophisticated method of observing changes in your data model.
Key Value Observing
I told you that design patterns run deep in iOS and Objective-C. You’re about to find out just how deep. MVC communications is based, in part, on the observer pattern. The observer pattern is a design pattern in which oneobject (the observer) receives a message when some event in another object (the subject) occurs. In MVC, the data model (the subject) notifies view or controller objects (the observers) whenever it changes. This relieves thecontroller from having to remember to update the view objects ([self updateColor]) whenever it changes the data model. Now it, or any other object, can just change the data model at will; any changes will send notifications tothe observers.
In MyStuff you accomplished this using NSNotifcation objects. In ColorModel you’re going to use some Objective-C magic called Key Value Observing (KVO for short). KVO is a technology that notifies an observer whenever theproperty of an object is set. The amazing thing about KVO is that you (usually) don’t have to make any changes to your data model objects; Objective-C and iOS do all of the work for you.
Observing Key Value Changes
Observing property changes in an object is a two-step process:
1. Become an observer for the property (key value).
2. Implement an -observeValueForKeyPath:ofObject:change:context: method.
The first step is simple enough. In your CMViewController.m implementation file, find the
-viewDidLoad method and add the following code at the end:
[_colorModel addObserver:self forKeyPath:@"hue" options:0 context:NULL]; [_colorModel addObserver:self forKeyPath:@"saturation" options:0 context:NULL]; [_colorModel addObserver:self forKeyPath:@"brightness" options:0 context:NULL]; [_colorModel addObserver:self forKeyPath:@"color" options:0 context:NULL];
Each statement registers your CMViewController object (self) to observe changes to a specific property (the key path) of the receiving object (_colorModel).
Thereafter, every time one of the observed properties of _colorModel changes, your controller will receive an -observeValueForKeyPath:ofObject:change:context: message. The keyPath parameter describes the property that changedon the object parameter. Use these parameters to determine what changed and take the appropriate action.
Your new -observeValueForKeyPath:ofObject:change:context: method will replace your old
-updateColor method, because it serves the same purpose. Replace –updateColor with the code in Listing 8-3. The code in bold shows what’s new.
Listing 8-3. -observeValueForKeyPath
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"hue"])
{
self.hueLabel.text = [NSString stringWithFormat:@"%.0f\u00b0", self.colorModel.hue];
}
else if ([keyPath isEqualToString:@"saturation"])
{
self.saturationLabel.text = [NSString stringWithFormat:@"%.0f%%", self.colorModel.saturation];
}
else if ([keyPath isEqualToString:@"brightness"])
{
self.brightnessLabel.text = [NSString stringWithFormat:@"%.0f%%", self.colorModel.brightness];
}
else if ([keyPath isEqualToString:@"color"])
{
[self.colorView setNeedsDisplay]; CGFloat red, green, blue, alpha;
[self.colorModel.color getRed:&red green:&green blue:&blue alpha:&alpha]; self.webLabel.text = [NSString stringWithFormat:@"#%02lx%02lx%02lx",
lroundf(red*255), lroundf(green*255), lroundf(blue*255)];
}
}
The code is simple and straightforward. It checks to see if the keyPath parameter matches one of the property names that you expect to change. Each if block updates the view objects affected by changes to that property.
You can now remove all of the references to -updateColor. Delete the method prototype in the private @interface section, and remove all of the [self updateColor]; statements in the action methods. Now your -changeHue:method looks like this:
- (IBAction)changeHue:(UISlider*)sender
{
self.colorModel.hue = sender.value;
}
None of your methods that change the properties of your data model have to remember to update the view, because the data model object will notify your controller automatically whenever that happens. Run your app and try itout, as shown in Figure 8-26.
Figure 8-26. Defective KVO
Some parts of it work, but clearly something is wrong. Let’s think about the problem for a moment.
Creating KVO Dependencies
Your controller is receiving changes for the hue, saturation, and brightness properties, because the three label objects are getting updated. The colorView and webLabel objects, however, never change. Your controller is notreceiving change notifications for the “color” property.
That’s because nothing ever changes the color property. (It’s not even allowed to change, because it’s a readonly property.) The problem is that color is a synthesized property value: code, that
you wrote, makes up the color value based on the values of hue, saturation, and brightness. Objective-C and iOS don’t know that. All they know is that no one ever sets the color property (colorModel.color = newColor), so itnever sends any notifications.
There are two, straightforward, ways to address this. The first would be to add code to your controller so that it updates the color-related views whenever it receives notifications that any of
the other three (hue, saturation, or brightness) changed. That’s a perfectly acceptable solution, but
there’s an alternative.
You can teach the KVO system about a property (the derived key) that is affected by changes to other properties (its dependent keys). Open your CMColor.m data model implementation file and add this special class method:
+ (NSSet*)keyPathsForValuesAffectingColor
{
return [NSSet setWithObjects:@"hue",@"saturation",@"brightness",nil];
}
Now run your app again and see the difference (see Figure 8-27) that one method makes.
Figure 8-27. Working KVO updates
So what’s happening? The special class method +keyPathsForValuesAffectingColor tell the KVO system that there are three properties (key paths) that affect the value of the color property: “hue”, “saturation”,and “brightness”.Now, whenever the KVO mechanism sees one of the first three properties change, it knows that color changed too and it sends a second notification for the “color” key path.
I’m sure you’re thinking this is pretty cool, but you might also be thinking that it’s not that much less work than the -updateColor method you wrote in the previous section. And you’re right; it’s not.
But that’s also because all of your data model changes come from one source (the slider controls),
and there’s a relatively small number of places where your data model is altered. If that situation changes, however, it becomes a whole new ballgame.
Multi-Vector Data Model Changes
As your app matures, it’s likely to get more complex, and changes to your data model can occur in more places. The beauty of KVO is that the change notifications happen in the same place the changes occur—in the datamodel.
It was OK to call -changeColor when the only places that changed the color were the three slider actions. But what if you added a fourth control view object that also changed them—or five, or nine?
Here’s an example. The sliders in your app are nice, but they’re sooooo Twentieth Century. We live in the age of the touch interface. Wouldn’t it be nicer to just touch the hue/saturation graph and point
to the color you want? Let’s do it.
Handling Touch Events
You should already know how to implement this—unless you skipped Chapter 4. If you did, go back and read it now. Add touch event handler methods to your custom CMColorView class. The handlers will use the coordinateswithin the color chart to choose a new hue and saturation. Since you know what you’re doing, get started by adding three touch event handlers to CMColorView.m:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self changeHSToPoint:[(UITouch*)[touches anyObject] locationInView:self]];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[self changeHSToPoint:[(UITouch*)[touches anyObject] locationInView:self]];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self changeHSToPoint:[(UITouch*)[touches anyObject] locationInView:self]];
}
These three handlers catch all touch began, moved, and ended events, extract the one touch object, gets the position of that touch in this view’s coordinates (using -locationInView:), and passes those coordinates to the -changeHSToPoint: method.
Now add the -changeHSToPoint: method. Start by adding a method prototype to the private
@interface section near the top of the CMColorView.m file:
- (void)changeHSToPoint:(CGPoint)point;
Finally, add the body of the method at the end of the @implementation:
- (void)changeHSToPoint:(CGPoint)point
{
CGRect bounds = self.bounds;
if (CGRectContainsPoint(bounds,point))
{
_colorModel.hue = (point.x-bounds.origin.x)/bounds.size.width*360;
_colorModel.saturation = (point.y-bounds.origin.y)/bounds.size.height*100;
}
}
Your -changeHSToPoint: method takes the touch point and works backwards to determine the hue and saturation values represented at the position. It then changes the hue and saturation properties of the data model directly.
Notice that it didn’t send an action message to the controller. It could have—that would be a perfectly reasonable implementation too. But since you have KVO, you don’t need to. Any object can make changes to the datamodel directly, and all the observers will receive the necessary notifications.
Try it out. Run your app. Move the brightness slider off of 0%, and then use the mouse to drag around inside the color chart. The hue and saturation change as you drag your (simulated) finger around, as shown in Figure 8-28.
Figure 8-28. Turning CMColorView into a control
Binding The Sliders
The only thing that doesn’t work is the hue and saturation sliders don’t move when you touch the color view. That’s because they’re still acting only as inputs. Up until this point, the only way the hue and saturation could havechanged was to move the slider. Now that there are other pathways to changing these properties, you need to keep the sliders in synchronization with the data model too.
You’ll need a connection to the three sliders, so add that to your CMViewController.h file:
@property (weak,nonatomic) IBOutlet UISlider *hueSlider;
@property (weak,nonatomic) IBOutlet UISlider *saturationSlider;
@property (weak,nonatomic) IBOutlet UISlider *brightnessSlider;
Switch to the Main.storyboard Interface Builder file and connect these new outlets from your View Controller object to the three UISlider controls.
Find the -observeValueForKeyPath:ofObject:change:context: method in CMViewController.m and insert these three lines of bold code:
if ([keyPath isEqualToString:@"hue"])
{
self.hueLabel.text = [NSString stringWithFormat:@"%.0f\u00b0", self.colorModel.hue];
self.hueSlider.value = _colorModel.hue;
}
else if ([keyPath isEqualToString:@"saturation"])
{
self.saturationLabel.text = [NSString stringWithFormat:@"%.0f%%", self.colorModel.saturation];
self.saturationSlider.value = _colorModel.saturation;
}
else if ([keyPath isEqualToString:@"brightness"])
{
self.brightnessLabel.text = [NSString stringWithFormat:@"%.0f%%", self.colorModel.brightness];
self.brightnessSlider.value = _colorModel.brightness;
}
Now when the hue value changes, the hue slider will be changed to match, even if the change came from the hue slider.
The color view and the sliders now update whenever the data model changes, and the color view can directly change the data model. Software engineers would say that these views are bound to properties of the data model.
Abinding is a direct, two-way, linkage between a data model and a view.
Final Touches
You can now also easily fix an annoying bug in your app. The display values for the hue, saturation, and brightness are wrong (360°, 100%, and 100%) when the app starts. The values in the data model are 0°, 0%, and 0%. Atthe very end of -viewDidLoad, add this code:
_colorModel.hue = 60;
_colorModel.saturation = 50;
_colorModel.brightness = 100;
Since this code executes after your controller starts observing changes to your data model, these statements will not only initialize your data model to a color that’s not black, but will also update all relevant views to match.Try it!
There’s also some icon resources in the Learn iOS Development Projects ➤ Ch 8 ➤ ColorModel (Icons) folder. Add them to the AppIcon group of the Images.xcassets item, just as you did for earlier projects.
Cheating
The model-view-controller design pattern will improve the quality of your code, make your apps simpler to write and maintain, and give you an attractive, healthy, glow. Do not, however, fall under its spell and become its slave.
While the use of design patterns gives you an edge in your quest to become a master iOS developer, I caution against using them just for the sake of using them. Pragmatic programmers call this over engineering. Sometimes the simple solutions are the best. Take this example:
@interface MyScoreController : NSObject
@property NSUInteger score;
@property (weak,nonatomic) IBOutlet UILabel *scoreView;
- (IBAction)incrementScore:(id)sender;
@end
@implementation MyScoreController
- (IBAction)incrementScore:(id)sender
{
}
@end
_score += 1;
self.scoreView.text = [NSString stringWithFormat:@"%lu",(unsigned long int)_score];
So what’s wrong with this controller? MVC purists will point out that there’s no data model object. The controller is also acting as the data model, storing and manipulating the score property. This violates the MVC designpattern as well as the single responsibility principle.
Do you want my opinion? There’s nothing wrong with this solution; it’s just one #@$%&* integer! There’s nothing to be gained in creating a new class just to hold one number, and you’ll waste a whole lot of time doing it.
If, someday, maybe, your data model grew to three integers, a string, and a localization object,
then sure: refactor your app, pull the integer out of your controller, and move it to a real data model object. But until that day arrives, don’t worry about it.
There’s a programming discipline called agile development that values finished, working, software over plans and pristine design. In these situations, my advice is to use the simplest solution that does the job. Be aware when you’re taking shortcuts in your MVC design, and have a plan to fix
it when (and if) that becomes a problem, but don’t lash yourself to a design philosophy. Design patterns should make your development easier, not harder.
Summary
To summarize: MVC is good.
Is all of this computer science study making you want to take a break and listen to your favorite tunes? Well then, the next chapter is for you.
While your ColorModel app came very close to the idealized MVC communications, it still relied on the controller to observe the changes and forward update events the view objects. Given the work you’ve done so far, howdifficult would it be to make the color view observe data model changes directly?
It wouldn’t be that hard, and that’s your exercise for this chapter: Modify CMViewController and CMColorView so that
CMColorView is the direct observer of “color” changes in the CMColor object.
This is a common pattern in fairly extensive apps that have complex data models and lots of custom view objects. The advantage is that each view object takes on the responsibility of observing the data model changes specificto that view, relieving your controller objects from this burden.




No comments:
Post a Comment