The last post was about drawing inner shadows on the inside of a known shape to give the effect that the shape was embossed, or indented. The approach taken was quite a lot of work, and only works for known shapes; we couldn’t for instance indent an image or letters with this approach. We also need to write new code for any shape that we wish to indent. It would be nice to have an object that would do all of this work for us for any given shape.

To automatically create inner shadows we will in some form have to do our own drawing. We can do this by making a subclass of CALayer. The way drawing works on a CALayer is you call setNeedsDisplay on your layer, this adds an item to the runloop to call display on your layer the next time round the run loop. Any subsequent calls to setNeedsDisplay won’t do anything until display has been called by the runloop. display works by calling your drawing code and caching the result in the layers’ content, then display won’t be called again unless the setNeedsDisplay method is called. To do custom drawing we can do all of this ourselves with our own customisations.

The first thing that we are going to do in our display method is to create a CGContextRef we can then pass this to either the delegates drawLayer:inContext:, or the layers’ drawInContext: method which will do the normal drawing. Here’s the code:

Override display Method
CGRect bounds = [self bounds];
CGFloat width = ceilf (bounds.size.width);
CGFloat height = ceilf (bounds.size.height);
uint8_t *dataBytes = malloc (width * height * sizeof(uint32_t));
CGFloat bytesPerDrawingRow = width * 4;
CGFloat bytesPerDrawingComponent = 4;
CGColorSpaceRef rgbSpace = CGColorSpaceCreateDeviceRGB ();
CGContextRef bitmapCtx = CGBitmapContextCreate (dataBytes,
width,
height,
8,
bytesPerDrawingRow,
rgbSpace,
kCGImageAlphaPremultipliedLast);

To make this work for a retina device you will need twice as many pixels in each direction and to set a transform to make the drawing happen at the right size.

Next we need to call the appropriate drawing method:

Call the Correct Drawing Method
id delegate = [self delegate];
CGImageRef mask;
CGContextSaveGState (bitmapCtx);
if (delegate && [delegate respondsToSelector:@selector (displayLayer:)]) {
[delegate displayLayer:self];
id content = [self contents];
if ([content isKindOfClass:[UIImage class]])
mask = (CGImageRef)CFRetain ((__bridge CGImageRef)[self contents]);
else {
// CGLayers save things in a backing form that isn't documented normally.
CFRelease (rgbSpace);
CFRelease (bitmapCtx);
free (dataBytes);
[super display];
return;
}
} else {
if (delegate && [delegate respondsToSelector:@selector(drawLayer:inContext:)])
[delegate drawLayer:self inContext:bitmapCtx];
else
[self drawInContext:bitmapCtx];
mask = CGBitmapContextCreateImage (bitmapCtx);
}
CGContextRestoreGState (bitmapCtx);

If at this point we called setContents:mask then we would have effectively re-implemented the original display method. Probably less efficiently, with some memory leaks!

We can now manipulate what has been drawn to create the shadows that we to draw. To draw the outside shadow we will draw mask with the context set to draw the correct shadow. To draw the inside shadow we invert the alpha channel of image data, clip to mask, set up the shadows that we want, and draw the inverted alpha image.

First we will create the image with the inverted alpha channel. We have all the bitmap data of the drawn image in dataBytes, so we can fiddle with this directly.

Invert the Alpha Channel
union dataUnion {
uint32_t intVal;
uint8_t byteVals[4];
};
union dataUnion baseData;
baseData.byteVals[0] = 0.0;
baseData.byteVals[1] = 0.0;
baseData.byteVals[2] = 0.0;
unsigned inverseSize = width * height * sizeof (uint32_t);
uint32_t *shadowImageData = malloc(inverseSize);
// Create the inverse mask. This does a lot of operations, possibly could be
// done with OpenGL shader, or using the Accelerate framewok.
for (int row = 0; row < height; ++row) {
for (int column = 0; column < width; ++column) {
if (clipForAnyAlpha) {
if (dataBytes[(int)(row * bytesPerDrawingRow + column * bytesPerDrawingComponent) + 3] > 1)
baseData.byteVals[3] = 0;
else
baseData.byteVals[3] = 255;
} else {
baseData.byteVals[3] = 255 - dataBytes[(int)(row * bytesPerDrawingRow + column * bytesPerDrawingComponent) + 3];
}
shadowImageData[(int)(row * width + column)] = baseData.intVal;
}
}
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL,
shadowImageData,
inverseSize,
dataRelease);
CGImageRef image = CGImageCreate (width,
height,
8,
32,
4 * width,
rgbSpace,
kCGImageAlphaPremultipliedLast,
dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
CFRelease (dataProvider);

Next we are going to draw the outside shadow. If we want to make it so that the original image isn’t drawn then we can clip to the inverted image before we draw the original data (which is in mask).

Draw original image with outside shadow
CGContextClearRect (bitmapCtx, bounds);
/* Flip the y-axis so that things draw the right way up. */
CGContextConcatCTM (bitmapCtx, CGAffineTransformMake (1.0, 0.0,
0.0, -1.0, 0.0, height));
CGContextSaveGState(bitmapCtx);
if (!drawOriginalImage)
CGContextClipToMask (bitmapCtx, bounds, image);
CGColorRef outsideShadow;
if (!outsideShadowColor) {
CGFloat whiteCol[] = { 1.0, 1.0, 1.0, 0.4 };
outsideShadow = CGColorCreate (rgbSpace, whiteCol);
} else
outsideShadow = (CGColorRef)CFRetain ([outsideShadowColor CGColor]);
CGSize shadowSize = CGSizeMake (0.0, -1.0);
CGFloat radius = 2.0;
if (outsideShadowSize) {
[outsideShadowSize getValue:&shadowSize];
shadowSize = CGSizeMake (shadowSize.width, -shadowSize.height);
}
if (outsideShadowRadius)
radius = [outsideShadowRadius floatValue];
CGContextSetShadowWithColor (bitmapCtx, shadowSize, radius, outsideShadow);
CGContextDrawImage (bitmapCtx, bounds, mask);
CGContextRestoreGState (bitmapCtx);

Next clip to the original image and draw the inverted image with a shadow, this will make the inside shadow.

Draw the inside shadow.
CGContextSaveGState(bitmapCtx);
CGContextClipToMask (bitmapCtx, bounds, mask);
shadowSize = CGSizeMake (0.0, -2.0);
radius = 2.0;
if (insideShadowSize) {
[insideShadowSize getValue:&shadowSize];
shadowSize = CGSizeMake (shadowSize.width, -shadowSize.height);
}
if (insideShadowRadius)
radius = [insideShadowRadius floatValue];
CGColorRef shadowColor;
if (insideShadowColor)
shadowColor = (CGColorRef)CFRetain ([insideShadowColor CGColor]);
else {
CGFloat defaultVals[] = { 0.0, 0.0, 0.0, 0.75 };
shadowColor = CGColorCreate (rgbSpace, defaultVals);
}
CGContextSetShadowWithColor (bitmapCtx, shadowSize, radius, shadowColor);
CGContextDrawImage (bitmapCtx, bounds, image);
CGContextRestoreGState(bitmapCtx);

Finally clear up, and set the result so that it is drawn to screen.

Set the image for drawing and clear up
CFRelease (shadowColor);
CFRelease (mask);
CFRelease (outsideShadow);
CFRelease (image);
CGImageRef result = CGBitmapContextCreateImage (bitmapCtx);
CFRelease (bitmapCtx);
[self setContents:(__bridge id)(result)];
CFRelease (result);
free (dataBytes);

Now for any view that does its drawing using the drawRect: method all we need to do to make inner shadows is to subclass the view, and swap its layer for our own. With what we’ve done if the view does its drawing in a different way, this layer won’t help us.

Here’s an example of a UILabel subclass changed to indent its content:

IndentLabel
#import "JTAIndentLabel.h"
#import "JTAInnerShadowLayer.h"
@implementation JTAIndentLabel
static void commonInit (JTAIndentLabel *self)
{
[self setBackgroundColor:[UIColor clearColor]];
JTAInnerShadowLayer *innerShadow = (JTAInnerShadowLayer *)[self layer];
[innerShadow setClipForAnyAlpha:YES];
[innerShadow setOutsideShadowSize:CGSizeMake(0.0, 1.0) radius:1.0];
[innerShadow setInsideShadowSize:CGSizeMake (0.0, 4.0) radius:6.0];
// Uncomment this to make the label also draw the text (won't work well
// with black text!
// [innerShadow drawOriginalImage];
}
+ (Class)layerClass
{
return [JTAInnerShadowLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
commonInit (self);
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super initWithCoder:aDecoder]))
commonInit (self);
return self;
}
@end

That’s it! here’s the result:

By changing the colour and size of the shadows we can emboss things instead of indenting them; resulting in something like this:

We can also make our own image view like this:

Indent image
- (id)initWithImage:(UIImage *)i andScale:(CGAffineTransform)scalingTrans
{
if ((self = [super initWithFrame:
CGRectApplyAffineTransform ((CGRect) {
(CGPoint){ 0.0, 0.0 },
[i size]},
scalingTrans)])){
JTAInnerShadowLayer *innerShadow = (JTAInnerShadowLayer *)[self layer];
insideShadSize = CGSizeMake (0.0, 4.0);
outsideShadowSize = CGSizeMake (0.0, 1.0);
[innerShadow setOutsideShadowSize:outsideShadowSize radius:1.0];
[innerShadow setInsideShadowSize:insideShadSize radius:6.0];
//[innerShadow setDrawOriginalImage:YES];
motionManager = [[CMMotionManager alloc] init];
[motionManager setAccelerometerUpdateInterval:1.0 / 60.0];
[motionManager startAccelerometerUpdates];
image = i;
scale = scalingTrans;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext ();
CGContextConcatCTM (ctx, CGAffineTransformMake (1.0, 0.0, 0.0, -1.0,
0.0, rect.size.height));
CGContextDrawImage (ctx, rect, [image CGImage]);
}

Here’s what the results look like:

With this CGLayer subclass we can quickly and very easily indent a lot of things. There are some possible issues with this. We use quite a lot of memory to manipulate the images, and we have to loop over every pixel in the image to invert the alpha channel. This may cause issues if you are doing this a lot, or if you have a very large drawing area.

You can find and use the code for this layer here. If you use this code please give me some credit for it.

Update

I’ve added a delegate method to the CALayer delegate protocol that allows you to draw whatever you want in the embossed, or indented region. The method is called drawToshadowedRegionInContext:. Here’s an example of using this method (this code is added to your UIView subclass):

example
- (void)drawToshadowedRegionInContext:(CGContextRef)ctx
{
CGContextDrawImage (ctx, [self bounds],
[[UIImage imageNamed:@"IMG_4229.jpg"] CGImage]);
}

I added this to my emboss view class, and this is the result:

I’ve committed these changes to the git repository, and the source is still
here

Update

Added Support for retina devices.