Interesting iPhone apps draw custom views/widgets. Every platform though makes assumptions that will be good for some applications and bad for others. As the Mac OS X evolved into iPhone OS, one of the graphics assumptions changed — the handedness of the drawing coordinate system. In Mac OS X’s case, the right handed coordinate system was a natural match to traditional graphing which we all learned in high school algebra. iPhone OS uses a left handed coordinate system. The left handed system is very good for drawing text. The big difference is in the direction of the y-axis. In a right handed system, the y-axis points up; a left handed y-axis points down. If you want to draw scatter plots, then a right handed system is just easier to use.
The graphics system is called Quartz 2D. It supports general purpose transformations of every graphics action. This is how the handedness can switch between the Mac and the iPhone — Quartz 2D made it easy. Hence, to return to a right handed system, we need to use this transformation system to flip the coordinate system. We will also need to move the origin of the view from the top-left to the bottom-left corner. Two tasks: flip the coordinate system and translate the origin.
Each view in Cocoa Touch draws in exactly one method: -drawRect:. Our code needs to intercept this method and transform the coordinate system. Also, because there is a common design pattern in Cocoa Touch, delegation, we should support that too. Finally, because of the ways views are implemented in Cocoa Touch, as CALayers, our class needs to provide some methods to make it easy for layers to be rendered in our new coordinate system.
Two tasks:
- Flip the coordinate system.
- Translate the origin.
Three requirements:
- Intercept -drawRect:
- Implement a delegate pattern.
- Provide a utility method for layer drawing.
The Right Hand View Header:
@interface RHView : UIView {
@private
id <RHViewDelegate> delegate;
}
@property (assign, nonatomic) id <RHViewDelegate> delegate;
// Abstract methods. This must be overridden.
- (void) drawRect: (CGRect) rect inContext: (CGContextRef) context;
// Concrete methods.
- (CGContextRef) flipContext: (CGContextRef) context;
@end
Starting at the top:
This class inherits from UIView, the standard parent class to all views in Cocoa Touch. It’s only instance variable is a pointer to a delegate that implements the RHViewDelegate protocol. As with almost all other delegate instance variables in Cocoa Touch, the delegate is assigned and not retained. It is up to the delegate to keep from being reaped.
This class has two public methods. The first method, -drawRect:inContext:, is an abstract method. It must be overridden by your subclass to draw anything. Or you must use the delegate. You cannot use both at the same time.
The second method, -flipContext:, is a utility method to transform a graphics context between coordinate systems. You should not override this method.
The alternate way to draw is with a delegate that implements the RHViewDelegate protocol.
@protocol RHViewDelegate
@required
// Draw contents into the given view, using the given bounds and context.
- (void) drawView: (RHView *) view inRect: (CGRect) rect inContext: (CGContextRef) context;
@end
Because a delegate is unrelated to the view, you need to pass the view to the delegate along with the rectangle that needs to be filled in and the flipped context: -drawView:inRect:inContext:.
How does RHView work?:
I’ll start with simpler of the 2 methods: -flipContext:.
- (CGContextRef) flipContext: (CGContextRef) context {
// Get the current transformation matrix.
CGAffineTransform ctm = CGContextGetCTM(context);
// Toggle the origin's position between the bottom-left and top-left.
CGAffineTransformTranslate(ctm, 0.0, self.bounds.size.height);
// Flip the handedness of the coordinate system.
CGAffineTransformScale(ctm, 1.0, -1.0);
// Apply the new coordinate system to the CGContext.
CGContextConcatCTM(context, ctm);
return context;
} // flipContext:
This method is rather straightforward. It implements our two tasks: flipping and translation. The key thing to recognize is that the new origin is always at the “top” of the current view. In a left handed system, the top-left point is the same as the bottom-left of a right handed system. And this is true, vice versa. The second transform, the CGAffineTransformScale, changes the sign of the y-axis. It changes the handedness of the system. That really is all that is required.
The RHView -drawRect: method has to do both the transformation and delegate dispatch. Unlike a normal UIView, you do not override -drawRect:. Because it is responsible for managing the coordinate system, it has to get the first crack at drawing the view. Hence, you need to override the -drawRect:inContext: method. The only difference between the two methods is that you will use the provided context instead of the default context. Other than that, everything about the new method is treated identically to the classical -drawRect: method. (A note: -drawRect:inContext: is using the same naming pattern as CALayer‘s delegate method -drawLayer:inContext:.)
- (void) drawRect: (CGRect) rect {
// Convert the coordinate system to be what Quartz 2D expects.
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform ctm = CGContextGetCTM(context);
// Translate the origin to the bottom left.
CGAffineTransformTranslate(ctm, 0.0, self.bounds.size.height);
// Flip the handedness of the coordinate system back to right handed.
CGAffineTransformScale(ctm, 1.0, -1.0);
// Convert the update rectangle to the new coordiante system.
CGRect xformRect = CGRectApplyAffineTransform(rect, ctm);
// Apply the new coordinate system to the CGContext.
CGContextConcatCTM(context, ctm);
if (delegate != nil) {
// The delegate gets the first crack at rendering the view.
[delegate drawView: self inRect: xformRect inContext: context];
} else {
[self drawRect: xformRect inContext: context];
}
} // drawRect:
-drawRect: has to do four tasks: 1) get the current graphics context; 2) translate and flip the coordinate system; 3) transform the drawing rectangle; 4) dispatch to either the delegate or subclass.
Getting the Code:
I’ve released this code into the public domain.
It is available here: RHView.zip.
Enjoy!