Along with the rest of the world, I’ve been coding on Snow Leopard recently. Blocks are a great addition to the C language, irrespective of their use in Apple’s Grand Central Dispatch APIs. I’ve found one usage-pattern where they save me time (I have little to spare), reduce the amount of mundane monkey-work I have to do , and make my code more reliable. In many places blocks can save me writing a whole line of code.
Transaction-based APIs
Cocoa is full of APIs that cause the developer to start a transaction, do some work and then end the transaction. Take for example drawing something with a shadow in an NSView:
- (void)drawRect:(NSRect)rect;
{
[[NSColor whiteColor] setFill];
NSRectFill([self bounds]);
[NSGraphicsContext saveGraphicsState];
[[self shadow] set];
[[NSColor redColor] setFill];
[[NSBezierPath bezierPathWithRect:NSInsetRect([self bounds], 10, 10)] fill];
[NSGraphicsContext restoreGraphicsState];
// do some more drawing
}
The important part of the above code is between the calls to save and restore the graphics state. Quartz (the underlying drawing API) is a state-machine, so if we were to set the shadow on our existing context, all subsequent drawing would be performed with said shadow. By saving the state, we can set a shadow temporarily and the continue to draw without a shadow by restoring the graphics context to its eariler state. It doesn’t stop with shadows, though; there are a multitude of setting we can apply to our contexts.
If you forget to call +[NSGraphicsContext restoreGraphicsState] after saving a state though then that makes me a sad panda. Try flipping the context’s transformation matrix and not restoring your state. You won’t win any design awards.
We have similar issues if we are manually notifying key-value observers of changes. All calls to -[NSObject willChangeValueForKey:] must be bracketed with calls to -[NSObject didChangeValueForKey:].
It doesn’t stop there, if you’ve done any work with Core Animation, its not uncommon to want to disable the implicit animations of layers, or to make a group or animations with longer durations than default. In these cases your code is littered with:
[CATransaction begin];
[CATransaction setDisableActions:YES];
// change any number of properties on your CALayers
[CATranaction commit];
This gets old, very quickly.
Enter Blocks
Lets make our code more robust, we all like robust. Blocks are anonymous functions, we can write them in-line and the can capture variables from their enclosing scope. In real-person terms we can write methods like this:
@implementation NSGraphicsContext (ESAdditions)
+ (NSGraphicsContext *)transactionUsingBlock:(void (^)(NSGraphicsContext * /* currentContext */))block;
{
[self saveGraphicsState];
block([self currentContext]);
[self restoreGraphicsState];
}
@end
In our app we can then do things like this:
- (void)drawRect:(NSRect)rect;
{
[[NSColor whiteColor] setFill];
NSRectFill([self bounds]);
[NSGraphicsContext transactionUsingBlock:^(NSGraphicsContext *currentContext){
[[self shadow] set];
[[NSColor redColor] setFill];
[[NSBezierPath bezierPathWithRect:NSInsetRect([self bounds], 10, 10)] fill];
}];
// do some more drawing
}
We’ve now done some drawing with a shadow, and we’ve not had to write those pesky calls to +[NSGraphicsContext saveGraphicsState] and +[NSGraphicsContext restoreGraphicsState].
Readability++
The wonderful side-effect of this is that the code executed as part of the block is indented by Xcode, too. Which means we can see, at a glance, the scope to which our transaction applies. Imagine doing the same for drawing into a CGLayer, your drawing code is in the block, and it all gets rendered into the layer. Blocks help you expresses your intent. (Yes, that’s a loaded statement. Feel free to quote me on this.).
You can download my categories and functions here. With a shiny New BSD license. Enclosed I have categories on:
NSGraphicsContext
CGContextRef
NSManagedObject (for writing your custom accessors)
NSObject (for KVO)
CATransaction
NSAnimationContext
NSTextStorage
Mutexes and semaphores would also be a great candidate for this. You can easily write your own equivalent of @synchronized, to work directly with pthread mutexes.
Mark Aufflick 14:38 on October 25, 2008 Permalink |
So happy to hear about the bibtex support! Oh, the animated views will be nice too, and thanks for sharing the example code.
Jonathan Dann 17:52 on October 25, 2008 Permalink |
Hi Mark,
Thanks for the encouragement! I’m getting really excited about this app. I just hope that I can get it out asap
Glad you like the sample code too. If you do manage to use it, let me know.
Brad Gibbs 17:45 on January 14, 2009 Permalink |
Thanks for the sample code. It’s been very helpful!
The sample project failed to compile initially. I changed a #import from Espresso/TLGradientView to TLGradientView and it compiled and ran fine.
I’m trying to integrate your code into an app. I’ve copied and pasted the AppController class in your sample project to an NSViewController subclass in my project. The app compiles and runs, but crashes when I try to expand or collapse an item. The Debugger stops on this line:
if (![(id)self.delegate respondsToSelector:@selector(outlineView:shouldCollapseItem:)])
The sample code I downloaded gives a warning stating that AppController may not conform to the TLAnimatingOutlineViewDelegate protocol. I added to my outline view controller, which shuts up the compiler, but the app still crashes.
Any thoughts on what I might be doing wrong?