Begin main content

Using blocks to simplify custom view drawing

Commonly when you are drawing a custom Cocoa view, you will be creating paths, filling and stroking them etc. You might even be drawing a similar shape a number of times. A naive approach to drawing five round rects with shadows might look like this:

#defineDDOFFSET 10
#defineDDWIDTH 50

- (void)drawRect:(NSRect)dirtyRect {

    [NSGraphicsContext saveGraphicsState];

    for (inti=0 ; i < 5 ; i++)
    {
        // offset and put onto pixel boundary
CGFloattop = [self frame].size.height - 0.5 - DDOFFSET;
        CGFloatleft = 0.5 + (DDWIDTH * i) + (DDOFFSET * i) + DDOFFSET;
                
        NSRectborderRect = NSMakeRect(left,
                                       DDOFFSET,
                                       DDWIDTH,
                                       top - DDOFFSET);
        
        NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:borderRect
                                                                   xRadius:5
                                                                   yRadius:5];
        
        NSColor *borderColor = [NSColor grayColor];
        
        NSShadow *borderShadow = [[NSShadow alloc] init];
        [borderShadow setShadowOffset:NSMakeSize(2.0, -2.0)];
        [borderShadow setShadowBlurRadius:2.0];
        [borderShadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
        
        NSColor *fillColor = [NSColor whiteColor];
        
        // do the drawing
        [borderColor set];
        [borderShadow set];
        [borderPath stroke];
        [fillColor set];
        [borderPath fill];
    }
    
    [NSGraphicsContext restoreGraphicsState];
}

(Note these examples are all using garbage collection).

Hopefully, it should occur to you that, amongst other issues, this is needlessly inefficient because a bunch of objects are being created and destroyed each time the view is dirtied. So we want to move the creation of objects into a separate method that is called when the view is created, store the paths in an array and the drawRect would then just have to set the colours, stroke the paths, etc. Something like this:

@interfaceDrawDemo2View : NSView {
    NSColor *borderColor;
    NSShadow *borderShadow;
    NSColor *fillColor;
    NSMutableArray *borderPaths;
}

@end
@implementationDrawDemo2View#defineDDOFFSET 10
#defineDDWIDTH 50

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        borderColor = [NSColor grayColor];

        borderShadow = [[NSShadow alloc] init];
        [borderShadow setShadowOffset:NSMakeSize(2.0, -2.0)];
        [borderShadow setShadowBlurRadius:2.0];
        [borderShadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
        
        fillColor = [NSColor whiteColor];
        
        borderPaths = [[NSMutableArray alloc] initWithCapacity:5];
        
        for (inti=0 ; i < 5 ; i++)
        {
            // offset and put onto pixel boundary
CGFloattop = [self frame].size.height - 0.5 - DDOFFSET;
            CGFloatleft = 0.5 + (DDWIDTH * i) + (DDOFFSET * i) + DDOFFSET;
            
            NSRectborderRect = NSMakeRect(left,
                                           DDOFFSET,
                                           DDWIDTH,
                                           top - DDOFFSET);
            
            NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:borderRect
                                                                       xRadius:5
                                                                       yRadius:5];
            
            [borderPaths addObject:borderPath];
        }
    }

    returnself;
}

- (void)drawRect:(NSRect)dirtyRect {

    [NSGraphicsContext saveGraphicsState];

    for (NSBezierPath *pathin borderPaths) {
                
        // do the drawing
        [borderColor set];
        [borderShadow set];
        [path stroke];
        [fillColor set];
        [path fill];
    }
    
    [NSGraphicsContext restoreGraphicsState];
}

@end

That's mildly painful, and disconnects the meaning of the above code somewhat, but it's not too bad. What, though, if there are other shapes that require different handling - well they will need their own array and drawing code. Then we want to subclass the view and the subclass wants to draw circles, etc. etc. Your view will become messy and very specific. Also in a real example I'd probably need more save/restore graphics states, but here the shadow on the fill isn't important.

Thankfully we can use blocks, which are what some other languages call closures, to keep the efficiency of separating the execution time of the drawing code while keeping it all together in a very general way. We end up with something almost identical to the first naive implementation, but the drawing section at the end goes into a block in an array, and the drawRect simply executes all those blocks:

@interfaceDrawDemoBlocksView : NSView {
    NSMutableArray *drawingBlocks;
}

- (void)addRoundRect: (int)num;

@end
@implementationDrawDemoBlocksView#defineDDOFFSET 10
#defineDDWIDTH 50

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        drawingBlocks = [[NSMutableArray alloc] initWithCapacity:5];

        for (inti=0 ; i < 5 ; i++)
            [self addRoundRect:i];
    }
    returnself;
}

- (void)drawRect:(NSRect)dirtyRect {

    for (void *(^block)(void) indrawingBlocks)
    {
        [NSGraphicsContext saveGraphicsState];
        block();
        [NSGraphicsContext restoreGraphicsState];
    }
}

- (void)addRoundRect: (int) num
{
    // offset and put onto pixel boundary
CGFloattop = [self frame].size.height - 0.5 - DDOFFSET;
    CGFloatleft = 0.5 + (DDWIDTH * num) + (DDOFFSET * num) + DDOFFSET;
    
    NSRectborderRect = NSMakeRect(left,
                                   DDOFFSET,
                                   DDWIDTH,
                                   top - DDOFFSET);
    
    NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:borderRect
                                                               xRadius:5
                                                               yRadius:5];
    
    NSColor *borderColor = [NSColor grayColor];
    
    NSShadow *borderShadow = [[NSShadow alloc] init];
    [borderShadow setShadowOffset:NSMakeSize(2.0, -2.0)];
    [borderShadow setShadowBlurRadius:2.0];
    [borderShadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
    
    NSColor *fillColor = [NSColor whiteColor];
    
    [drawingBlocks addObject:[^{

        // do the drawing
        [borderColor set];
        [borderShadow set];
        [borderPath stroke];
        [fillColor set];
        [borderPath fill];
        
    } copy]];
}

@end

You can also see how this would work really well if we subclassed this view - all subclasses could simply push a block onto the array to request some work to be done during the drawRect phase.

You'll note how the block itself isn't added to the array, but a copy of it. That's because by default the storage for the context embodied in the block is only valid for the scope in which it is created. If we want to execute the block after the scope is gone we need to copy it.

If you found this useful you might also like Drew McCormack's excellent article 10 Uses for Blocks in C/Objective-C.

11:36 PM, 29 May 2010 by Mark Aufflick Permalink

Add comment