0

I am trying to draw an inset shadow inside a rounded rectangle by clipping away the stroke of a path using a clipping mask, leaving only the shadow behind. The ultimate goal is to approximate the inset look of things in the notification center.

I'm failing. CGContextClipToMask is simply not working, which is especially maddening because I'm using this same technique elsewhere in the project, and it works fine there. Here is my drawing code:

const CGFloat cornerRadius = 5.0f;
CGContextRef context = UIGraphicsGetCurrentContext();

// Draw background color
[[UIColor colorWithWhite:0.2 alpha:0.5] setFill];
UIBezierPath* background = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:cornerRadius];
[background fill];

// Draw the inner shadow
UIImage* mask;
UIGraphicsBeginImageContext(self.bounds.size);
{{
    CGContextRef igc = UIGraphicsGetCurrentContext();
    [[UIColor blackColor] setFill];
    CGContextFillRect(igc, self.bounds);

    // Inset the mask by a ridiculous degree just to be sure that it's working
    UIBezierPath* insetMask = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, 20, 20) cornerRadius:cornerRadius];
    [[UIColor whiteColor] setFill];
    [insetMask fill];

    mask = UIGraphicsGetImageFromCurrentImageContext();
}}
UIGraphicsEndImageContext();

CGContextSaveGState(context);
{{
    CGContextClipToMask(context, self.bounds, mask.CGImage);

    [[UIColor whiteColor] setStroke];
    CGContextSetShadow(context, CGSizeMake(0, 1), 1);
    [background stroke];
}}
CGContextRestoreGState(context);

If I were to guess at what's wrong, I'd say I am somehow violating the prerequisites laid out in the docs:

If mask is an image, then it must be in the DeviceGray color space, may not have an alpha component, and may not be masked by an image mask or masking color.

However, I have discovered no way to verify whether this is the case, let alone correct it if so.

TIA for any help you can offer!

(I am running the app on an iPhone 4S running iOS 6.)

Ryan Ballantyne
  • 4,064
  • 3
  • 26
  • 27

1 Answers1

4

You can't create a single-channel image using UIGraphicsBeginImageContext. You have to use CGBitmapContextCreate. You are also not handling Retina properly by using UIGraphicsBeginImageContext instead of UIGraphicsBeginImageContextWithOptions.

Let me show you a perhaps simpler way to draw the inner shadow that doesn't require messing about with bitmap contexts and images. When you have a non-self-intersecting path, you can invert it by surrounding it with a large rectangle and using the even-odd fill rule.

Here's an example. I made an InnerShadowView. This is its drawRect::

- (void)drawRect:(CGRect)rect {
    [self fillIfNeeded];
    [self drawShadowIfNeeded];
}

The fillIfNeeded method just draws the solid rounded rectangle:

- (void)fillIfNeeded {
    UIColor *color = self.fillColor;
    if (!color)
        return;

    [color setFill];
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds
        cornerRadius:self.cornerRadius];
    [path fill];
}

The drawShadowIfNeeded method draws the inner shadow. First, I clip to the rounded rectangle:

- (void)drawShadowIfNeeded {
    UIColor *color = self.shadowColor;
    if (!color)
        return;

    CGContextRef gc = UIGraphicsGetCurrentContext();
    CGContextSaveGState(gc); {
        [[UIBezierPath bezierPathWithRoundedRect:self.bounds
            cornerRadius:self.cornerRadius] addClip];

Next, I construct an inverse of the rounded rectangle: a path that contains everything except the rounded rectangle. I start with a really big rectangle:

        UIBezierPath *invertedPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

Then I append the rounded rectangle to the same path:

        [invertedPath appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, -1, -1) cornerRadius:self.cornerRadius]];

Note that I'm expanding the rounded rectangle slightly. This is necessary when the corner radius is nonzero, to avoid some artifacts around the curves.

Next, I set the compound path to use the even/odd fill rule:

        invertedPath.usesEvenOddFillRule = YES;

By using the even/odd fill rule, I ensure that any points inside both the infinite rectangle and the rounded rectangle are excluded from the fill area, while any points outside the rounded rectangle are included.

Now, when I fill this inverted path, it will try to fill every pixel outside the rounded rectangle. The only place it can cast a shadow is inside the rounded rectangle. And since I've clipped to the rounded rectangle, it will only draw the shadow.

So I set up the fill parameters and fill the path:

        CGContextSetShadowWithColor(UIGraphicsGetCurrentContext(),
            self.shadowOffset, self.shadowBlur, color.CGColor);
        [[UIColor blackColor] setFill];
        [invertedPath fill];

And clean up:

    } CGContextRestoreGState(gc);
}

With a red shadow color, it looks like this:

InnerShadowView screen shot

You can find all the code in this gist for easy downloading.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848