by ampatspell
in Code
In my spare time I’m designing my first “real” Cocoa appplication and I’ve decided to be rather open about this app and it’s implementation details prior and after the release.
I’m not going to disclose anything about the app just yet. Well, maybe only that it will come with open-source cloud component deployable (or automatically deployed) to AppEngine.
While I’m still siting in Photoshop trying to come up with interface design, I wanted to create actual clickable buttons from mockup. Here is more or less step-by-step guide of drawing them.
But now about NSButton custom drawing.

In Cocoa NSButton is subclass of NSControl which are drawn using NSCells. So we need a new NSButtonCell subclass to do our drawing:
#import "AMDarkButtonCell.h" @implementation AMDarkButtonCell - (void)drawImage:(NSImage*)image withFrame:(NSRect)frame inView:(NSView*)controlView { } - (NSRect)drawTitle:(NSAttributedString*)title withFrame:(NSRect)frame inView:(NSView*)controlView { } - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView { } @end
My not so great custom button consists of only of image and bezel so I’m not going to override -drawTitle:withFrame:inView:.
Lets start with bezel. It’s composed from:
Cocoa provides:
NSBezierPath for drawing, stroking and clipping-to rounded rectanglesNSGradient for drawing multiple point gradientsThat’s basically all we need to draw whole background composite.
- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView { NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; CGFloat roundedRadius = 3.0f; // Outer stroke (drawn as gradient) [ctx saveGraphicsState]; NSBezierPath *outerClip = [NSBezierPath bezierPathWithRoundedRect:frame xRadius:roundedRadius yRadius:roundedRadius]; [outerClip setClip]; NSGradient *outerGradient = [[NSGradient alloc] initWithColorsAndLocations: [NSColor colorWithDeviceWhite:0.20f alpha:1.0f], 0.0f, [NSColor colorWithDeviceWhite:0.21f alpha:1.0f], 1.0f, nil]; [outerGradient drawInRect:[outerClip bounds] angle:90.0f]; [outerGradient release]; [ctx restoreGraphicsState]; // Background gradient [ctx saveGraphicsState]; NSBezierPath *backgroundPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.0f, 2.0f) xRadius:roundedRadius yRadius:roundedRadius]; [backgroundPath setClip]; NSGradient *backgroundGradient = [[NSGradient alloc] initWithColorsAndLocations: [NSColor colorWithDeviceWhite:0.17f alpha:1.0f], 0.0f, [NSColor colorWithDeviceWhite:0.20f alpha:1.0f], 0.12f, [NSColor colorWithDeviceWhite:0.27f alpha:1.0f], 0.5f, [NSColor colorWithDeviceWhite:0.30f alpha:1.0f], 0.5f, [NSColor colorWithDeviceWhite:0.42f alpha:1.0f], 0.98f, [NSColor colorWithDeviceWhite:0.50f alpha:1.0f], 1.0f, nil]; [backgroundGradient drawInRect:[backgroundPath bounds] angle:270.0f]; [backgroundGradient release]; [ctx restoreGraphicsState]; // Dark stroke [ctx saveGraphicsState]; [[NSColor colorWithDeviceWhite:0.12f alpha:1.0f] setStroke]; [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 1.5f, 1.5f) xRadius:roundedRadius yRadius:roundedRadius] stroke]; [ctx restoreGraphicsState]; // Inner light stroke [ctx saveGraphicsState]; [[NSColor colorWithDeviceWhite:1.0f alpha:0.05f] setStroke]; [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.5f, 2.5f) xRadius:roundedRadius yRadius:roundedRadius] stroke]; [ctx restoreGraphicsState]; // Draw darker overlay if button is pressed if([self isHighlighted]) { [ctx saveGraphicsState]; [[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 2.0f, 2.0f) xRadius:roundedRadius yRadius:roundedRadius] setClip]; [[NSColor colorWithCalibratedWhite:0.0f alpha:0.35] setFill]; NSRectFillUsingOperation(frame, NSCompositeSourceOver); [ctx restoreGraphicsState]; } }
To draw image I’ve chosen to use it only as a mask for 2 draw operations:
- (void)drawImage:(NSImage*)image withFrame:(NSRect)frame inView:(NSView*)controlView { NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; CGContextRef contextRef = [ctx graphicsPort]; NSData *data = [image TIFFRepresentation]; // open for suggestions CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)data, NULL); if(source) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL); CFRelease(source); // Draw shadow 1px below image CGContextSaveGState(contextRef); { NSRect rect = NSOffsetRect(frame, 0.0f, 1.0f); CGFloat white = [self isHighlighted] ? 0.2f : 0.35f; CGContextClipToMask(contextRef, NSRectToCGRect(rect), imageRef); [[NSColor colorWithDeviceWhite:white alpha:1.0f] setFill]; NSRectFill(rect); } CGContextRestoreGState(contextRef); // Draw image CGContextSaveGState(contextRef); { NSRect rect = frame; CGContextClipToMask(contextRef, NSRectToCGRect(rect), imageRef); [[NSColor colorWithDeviceWhite:0.1f alpha:1.0f] setFill]; NSRectFill(rect); } CGContextRestoreGState(contextRef); CFRelease(imageRef); } }
Ok, now we have drawing code in place, lets move to Interface Builder and wire up this custom NSButtonCell for buttons which should be drawn with this style.
Add some buttons:

Set Image attribute in “Button Attributes” panel:

Click one more time on button in window to select it’s Cell Identity:

And set cell Class to our custom implementation:

That’s it.