Challenges - Bronze solution / Silver and Gold clarification


#1

Hi

Here’s my bronze solution:
Inside HypnosisView.m initWithFrame method

[code]CALayer *bronzeLayer = [[CALayer alloc] init];
CGRect bronzeBounds = CGRectMake(0.0, 0.0, boxLayer.bounds.size.width / 2, boxLayer.bounds.size.height / 2);
[bronzeLayer setBounds:bronzeBounds];
[bronzeLayer setAnchorPoint:CGPointMake(-0.5, -0.5)];

    UIColor *blueish = [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.5];
    CGColorRef cgBlueish = [blueish CGColor];
    [bronzeLayer setBackgroundColor:cgBlueish];
    
    UIImage *bronzeImage = [UIImage imageNamed:@"Time.png"];
    CGImageRef image2 = [bronzeImage CGImage];
    [bronzeLayer setContents:(__bridge id)image2];
    [bronzeLayer setContentsRect:[boxLayer contentsRect]];
    [bronzeLayer setContentsGravity:kCAGravityResizeAspect];
    
    [boxLayer addSublayer:bronzeLayer];[/code]

For the silver and gold challenges, not exactly sure what is meant by "Have the CALayer draw with…"
Does it mean just give the boxLayer a corner radius? But this one-liner wouldn’t warrant a silver challenge!

or have CALayer automatically give any of it’s newly created layers a corner radius?

Same with the gold challenge - adding:

UIColor *shadow = [UIColor blackColor]; CGColorRef shadowColour = [shadow CGColor]; [boxLayer setShadowColor:shadowColour]; [boxLayer setShadowOffset:CGSizeMake(0, 3)]; [boxLayer setShadowOpacity:0.8]; [boxLayer setShadowRadius:5.0];
gives the image a shadow; but surely this isn’t the solution for a gold! Even if you need to add a mask with masksToBounds it doesn’t seem as taxing as the previous chapter challenges. Have I missed the point somewhere?

Thanks, Mark


#2

Here’s what I did for the Gold challenge:

/* (initWithFrame:) ...snip... */

// Define a radius for the shadow
float shadowRadius = 5.0;

// Define two paths for the shadow in order to mask the inside
// of the shadow
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:[boxLayer bounds]
                                                      cornerRadius:[boxLayer cornerRadius]];
CGRect shadowBounds = CGRectInset([boxLayer bounds], -1 * shadowRadius, -1 * shadowRadius);
UIBezierPath *shadowOuterPath = [UIBezierPath bezierPathWithRoundedRect:shadowBounds
                                                           cornerRadius:[boxLayer cornerRadius]];

// Since the shadow is drawn using the non-zero rule,
// invert one of the shadow paths in order to create a cutout effect
[shadowOuterPath applyTransform:CGAffineTransformMakeScale(1.0, -1.0)];
CGFloat translationDelta = [shadowOuterPath bounds].size.height - (shadowRadius * 2.0);
[shadowOuterPath applyTransform:CGAffineTransformMakeTranslation(0.0, translationDelta)];
[shadowPath appendPath:shadowOuterPath];

// Setup and add shadow to the layer
[boxLayer setShadowPath:[shadowPath CGPath]];
[boxLayer setShadowColor:[[UIColor greenColor] CGColor]];
[boxLayer setShadowOpacity:0.67];
[boxLayer setShadowOffset:CGSizeZero];
[boxLayer setShadowRadius:shadowRadius];

/* ...snip... */

The difficult part was figuring out how to create an “inverted mask” for the shadow (much reading and seating Google about CALayer, UIBezierPath, and CGPathRef). I made the shadow color green in order to tell more easily if the layer overlapped the shadow.


#3

Hi artisonian

Thanks for the reply. My colours were too subtle to see the full effect of the shadow. When I changed it to green and reduced the opacity of the reddish backgroundColor, the green shadow showed up around the white spiral graphic in the centre. I thought there was more to it than that! And quite a bit more to it from your post! Thanks again for your help.

Did I also miss the point regarding the Silver Challenge? My one-line answer surely can’t be the solution!?

Thanks, Mark


#4

@ artisonian

Your solution isn’t correct, though. It’s easy to see it. Change the boxLayer’s opacity to 0.1,

and increase the shadowOpacity to 1.0:

Do you see it? The inner margin of the shadow has a blur that intrudes into the boxLayer. What’s worse, try offsetting the shadow, and you’ll see it drawing through the boxLayer. Here, I’ve simply given a real offset instead of CGRectZero to something admittedly extreme just to highlight what’s happening, and I’ve changed the shadow to blue, since green was a little too subtle for my eyes:

[code] /* (initWithFrame:) …snip… */

	// Define a radius for the shadow
	float shadowRadius = 5.0;
	CGSize shadowOffset = CGSizeMake(20.0, 20.0);

	// Define two paths for the shadow in order to mask the inside
	// of the shadow
	UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:[boxLayer bounds]
														  cornerRadius:[boxLayer cornerRadius]];
	CGRect shadowBounds = CGRectInset([boxLayer bounds], -1 * shadowRadius, -1 * shadowRadius);
	UIBezierPath *shadowOuterPath = [UIBezierPath bezierPathWithRoundedRect:shadowBounds
															   cornerRadius:[boxLayer cornerRadius]];
	
	// Since the shadow is drawn using the non-zero rule,
	// invert one of the shadow paths in order to create a cutout effect
	[shadowOuterPath applyTransform:CGAffineTransformMakeScale(1.0, -1.0)];
	CGFloat translationDelta = [shadowOuterPath bounds].size.height - (shadowRadius * 2.0);
	[shadowOuterPath applyTransform:CGAffineTransformMakeTranslation(0.0, translationDelta)];
	[shadowPath appendPath:shadowOuterPath];
	
	// Setup and add shadow to the layer
	[boxLayer setShadowPath:[shadowPath CGPath]];
	[boxLayer setShadowColor:[[UIColor blueColor] CGColor]];
	[boxLayer setShadowOpacity:1.0];
	[boxLayer setShadowOffset:shadowOffset];
	[boxLayer setShadowRadius:shadowRadius];
	
	/* ...snip... */		

[/code]

I haven’t got the solution yet. But the setShadowPath doesn’t seem to be the right direction, since the resulting shadow is blurry on both sides, and we need something clipped precisely to the boxLayer.


#5

Hi all,

Here goes… I think I came up with a solution to make it so the shadow doesn’t show through the layer. I embedded the entire layer in another layer, and just made that top layer be partially transparent instead of the original layer. I called that new top layer “boxLayer” so I didn’t have to change all of the “touches” code.

Here’s the new code in HypnosisView.m:

[code]- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
// background color = “clear”
[self setBackgroundColor:[UIColor clearColor]];
[self setCircleColor:[UIColor lightGrayColor]];

    // Create a new layer object and give it a size & location
    boxLayer = [[CALayer alloc] init];
    [boxLayer setBounds:CGRectMake(0, 0, 85, 85)];
    [boxLayer setPosition:CGPointMake(160, 100)];
    
    CALayer *boxSubLayer = [[CALayer alloc] init];
    [boxSubLayer setBounds:CGRectMake(0, 0, 85, 85)];
    [boxSubLayer setPosition:CGPointMake(85/2.0, 85/2.0)];
    
     // Make the background color be **fully-opaque** red
    CGColorRef cgRed = [[UIColor redColor] CGColor];
    [boxSubLayer setBackgroundColor:cgRed];

    // Create a UIImage and get the underlying CGImage
    UIImage *layerImage = [UIImage imageNamed:@"Hypno.png"];
    CGImageRef image = [layerImage CGImage];
    
    // Put the image on our sublayer
    [boxSubLayer setContents:(__bridge id)image];
    
    // Inset the image a bit
    [boxSubLayer setContentsRect:CGRectMake(-0.1, -0.1, 1.2, 1.2)];
    
    // Let the image resize w/o changing the aspect ratio, to fill the contentRect
    [boxSubLayer setContentsGravity:kCAGravityResizeAspect];

    [boxSubLayer setShadowOffset:CGSizeMake(5, 5)];
    [boxSubLayer setShadowOpacity:1];

    // Ch 22: Silver Challenge: Add rounded corners
    [boxSubLayer setCornerRadius:10];
    
    // These two lines are the key. Make sure we render the sub layer completely
    // as a bitmap before applying the opacity. Otherwise the shadow still shows thru.
    [boxSubLayer setShouldRasterize:YES];
    [boxLayer setOpacity:0.5];

    [boxLayer addSublayer:boxSubLayer];
    
    // Make our new layer a sublayer of the view's layer
    [[self layer] addSublayer];
}

return self;

}
[/code]

The only “weird” line was:

[boxSubLayer setShouldRasterize:YES]; I think this line allows the image and its shadow to be “burned” into a raster, and then the entire thing can be made transparent.

Let me know if this doesn’t make sense, or if you find a bug!
Thanks,
Chris


#6

For the silver challenge, I agree, you could add just this line :

But then you have to do it with every CALayer that you instantiate … boring.

So what I did is create a subclass of CALayer, that I called myCALayer, in which I overrode the initializer :

myCALayer.h

[code]#import <QuartzCore/QuartzCore.h>

@interface myCALayer : CALayer

@end[/code]

myCALayer.m

[code]#import “myCALayer.h”

@implementation myCALayer

  • (id)init
    {
    self = [super init];

    if (self) {
    [self setCornerRadius:10.0];
    }
    return self;
    }

@end
[/code]

So each instance of myCALayer will first call [super init] (that is instantiate CALayer), and then add the specific attributes of myCALayer (cornerRadius set to 10).

In HypnosisView.m, I declared boxLayer and my subLayer like so:

boxLayer = [[myCALayer alloc]init];

import the myCALayer class : #import "myCALayer.h"


#7

Just after I posted my proposal to subclass CALAyer, I read the next “for the more curious”, and here I find “in practice, subclassing CALayer is the last thing you want to do” …
Does that apply to the situation of silver / Gold challenges, for which the init: method is overriden ?


#8

@ gorthmog/Chris,

Great ideas on your solution! It led me in the right direction, but I have two comments/changes.
[ul]
1.) I found a bug on my iPhone 4S (I think it has something to do with the retina display) where small line artifacts remain on the screen when I move the CALayer around. After much Google searching, I discovered that before you call the setShouldRasterize: method, it is usually a good idea to set the rasterization scale in the following way:

[boxLayer setRasterizationScale:[[UIScreen mainScreen] scale]];

[/ul]
[ul]
2.) You didn’t need to create the extra CALayer (what you call boxLayer in your solution). Here’s what I did:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // All HypnosisViews start with a clear background color
        [self setBackgroundColor:[UIColor clearColor]];
        [self setCircleColor:[UIColor lightGrayColor]];
        
        // Create the new layer object
        boxLayer = [[CALayer alloc] init];
        
        // Give it a size
        [boxLayer setBounds:CGRectMake(0.0, 0.0, 85.0, 85.0)];
        
        // Give it a location
        [boxLayer setPosition:CGPointMake(160.0, 100.0)];
        
        // Make *FULLY OPAQUE* red the background color for the layer
        UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
        
        // Get a CGColor object with the same color values
        CGColorRef cgReddish = [reddish CGColor];
        [boxLayer setBackgroundColor:cgReddish];
        
        // Create a UIImage
        UIImage *layerImage = [UIImage imageNamed:@"Hypno.png"];
        
        // Get the underlying CGImage
        CGImageRef image = [layerImage CGImage];
        
        // Put the CGImage on the layer
        [boxLayer setContents:(__bridge id)image];
        
        // Inset the image a bit on each side
        [boxLayer setContentsRect:CGRectMake(-0.1, -0.1, 1.2, 1.2)];
        
        // Let the image resize (without changing the aspect ratio)
        // to fill the contentRect
        [boxLayer setContentsGravity:kCAGravityResizeAspect];
        
        [boxLayer setCornerRadius:15.0];
        
        [boxLayer setShadowOpacity:1.0];
        [boxLayer setShadowOffset:CGSizeMake(10.0, 10.0)];
        [boxLayer setShadowColor:[[UIColor blueColor] CGColor]];
        
        [boxLayer setRasterizationScale:[[UIScreen mainScreen] scale]];
        [boxLayer setShouldRasterize:YES];
        [boxLayer setOpacity:0.6];
        
        // Make it a sublayer of the view's layer
        [[self layer] addSublayer];
        
        insetLayer = [[CALayer alloc] init];
        [insetLayer setBounds:CGRectInset([boxLayer bounds], 85.0/4.0, 85.0/4.0)];
        [insetLayer setPosition:CGPointMake(boxLayer.bounds.size.width*0.5, boxLayer.bounds.size.height*0.5)];
        
        UIImage *insetImage = [UIImage imageNamed:@"star.png"];
        CGImageRef iImage = [insetImage CGImage];
        [insetLayer setContents:(__bridge id)iImage];
        
        [boxLayer addSublayer:insetLayer];
        
    }
    return self;
}

[/ul]


#9

I’m trying to come up with a solution more like @Ronald’s: I agree setting the shadowPath won’t give a crisp enough edge, so I think we need to put the shadow in a different layer and then mask it (sharply).

(The @gorthmog and @pschluet solution, using CALayer opacity rather than a background color with alpha, looks great and makes sense, but I have a nit-pick: The Hypno.png image is now partially transparent, where it wasn’t before. In real life this would probably be fine. For this exercise, I want to try something different.)

So I make a new shadowLayer which has a shadowPath set to the shape of the boxLayer.

Then I do some drawing into a mask layer, and set it as the mask of the shadowLayer. The mask only allows the shadow outside the rounded rectangle to come through.

HypnosisView.m initWithFrame:
(Apologies, as elsewhere, for style differences with the book.)

    _boxLayer = [[CALayer alloc] init];
    _boxLayer.bounds = CGRectMake(0.0, 0.0, 84.0, 84.0);
    _boxLayer.position = CGPointMake(160.0, 100.0);
    _boxLayer.cornerRadius = 10.0;

    CGColorRef reddish = [[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5] CGColor];
    _boxLayer.backgroundColor = reddish;

    UIImage *layerImage = [UIImage imageNamed:@"Hypno.png"];
    CGImageRef image = [layerImage CGImage];
    _boxLayer.contents = (__bridge id)image;
    _boxLayer.contentsRect = CGRectMake(-0.1, -0.1, 1.2, 1.2);
    _boxLayer.contentsGravity = kCAGravityResizeAspect;

    // We want a shadow for the box layer, but its partial transparency
    // causes problems.  Put the shadow in another layer, and then mask
    // the shadow to the box layer's bounds.

    CALayer *shadowLayer = [[CALayer alloc] init];
    shadowLayer.shadowOffset = CGSizeMake(8.0, 6.0);  // twice as deep as text's shadow
    shadowLayer.shadowRadius = 2.0;  // twice as blurred as text's shadow
    shadowLayer.shadowOpacity = 1.0;
    const CGFloat kShadowBorder = MAX(shadowLayer.shadowOffset.width, shadowLayer.shadowOffset.height) + shadowLayer.shadowRadius + 5.0;
    // (Setting bounds here doesn't affect the generated shadow, because shadow can go outside bounds.
    // But it helps make the masking stuff more sensible.)
    shadowLayer.bounds = CGRectMake(0.0, 0.0, _boxLayer.bounds.size.width + kShadowBorder * 2, _boxLayer.bounds.size.height + kShadowBorder * 2);
    shadowLayer.position = CGPointMake(_boxLayer.bounds.size.width / 2, _boxLayer.bounds.size.height / 2);
    UIBezierPath *boxPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(kShadowBorder, kShadowBorder, _boxLayer.bounds.size.width, _boxLayer.bounds.size.height)
                                                       cornerRadius:_boxLayer.cornerRadius];
    shadowLayer.shadowPath = [boxPath CGPath];
    shadowLayer.shadowColor = [[UIColor darkGrayColor] CGColor];

    CALayer *maskLayer = [[CALayer alloc] init];
    maskLayer.bounds = shadowLayer.bounds;
    maskLayer.position = CGPointMake(shadowLayer.bounds.size.width / 2, shadowLayer.bounds.size.height / 2);
    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 maskLayer.bounds.size.width,
                                                 maskLayer.bounds.size.height,
                                                 8,
                                                 maskLayer.bounds.size.width,
                                                 NULL,
                                                 kCGImageAlphaOnly);
    UIGraphicsPushContext(context);
    CGContextSetAlpha(context, 1.0);
    CGContextFillRect(context, maskLayer.bounds);
    [boxPath fillWithBlendMode:kCGBlendModeCopy alpha:0.0];
    CGImageRef maskImage = CGBitmapContextCreateImage(context);
    maskLayer.contents = (__bridge id)maskImage;
    UIGraphicsPopContext();
    CGImageRelease(maskImage);
    CGContextRelease(context);

    shadowLayer.mask = maskLayer;
    [_boxLayer addSublayer:shadowLayer];
    [self.layer addSublayer:_boxLayer];

Extra notes. Feel free to skip unless you feel like brainstorming more on this.
[ul]
[li] The anti-aliasing on the mask at the corners of the rounded rectangle is wrong. In short: The box layer has rounded corners. Those corners are anti-aliased, meaning that some of the pixels have a non-zero alpha. But my mask at those pixels is fully blocking out the shadow image, so what you see through those pixels (e.g. at the rounded edge of the lower-right corner of the red box) is just background with no shadow. This looks wrong. (Look for it when the background is white at that corner: The anti-aliased pixels look too white.) The bad part of my code would seem to be the way I draw the Bezier curve on my mask using kCGBlendModeCopy. Is the Bezier curve properly anti-aliasing (and in the same way the box layer does) into the current graphics context? Is the blend mode allowing the curve’s alpha to be preserved at the key pixels? I’m not sure what I’m doing wrong.[/li]
[li] I’m using the mask property of the shadow layer to do my “erasing.” Instead, I could use a temporary shadow layer to generate the uncropped shadow, then render that image out into a graphics context, then do my erasing directly on that image, and then put the cropped shadow back into a the final shadow layer. (I tried this briefly but ran into problems.)[/li]
[li] It would be nice to create the mask just by composing CALayers (rather than using a CG context). Then for the rounded rectangle I could create a CALayer the same shape as the original box layer, and blend it in. However, I would need to blend it in using something like kCGBlendModeCopy, and I don’t think that CALayer sublayers support blending modes. Yet.[/li]
[li] After all this, let’s just say it: It’s weird that this thing has a transparent reddish window through which you can see everything but the shadow. I mean, if it’s really transparent, then you should be able to see everything through it, including the shadow, right? (To see what I mean: Hold the reddish box over the “You are getting sleepy” text. The box’s shadow blocks out, say, part of an “n” – but then you can see the rest of the “n” clearly through the reddish window. Weird.)[/li][/ul]


#10

Hi all,

I’ve been trying to solve this for ages, and this discussion helped me a lot. I just wanted to summarize everything posted here, and what I learned from Apple’s docs, to make it easier for people looking out for help in the future.

The main difficulty of this challenge is to make the shadow appear only on the outside of the CALayer, but having that our boxLayer’s contents are semi-transparent, the shadow ends up showing underneath it as well.

Apple’s documentation explicitly mentions this exact problem, suggests a solution and even shows a picture example with the exact solution we are looking for, but doesn’t go on to show how to do it :cry:. See developer.apple.com/library/ios … 4-CH10-SW1.

[size=85]Notice how the red shadow doesn’t show through the teal background, even though it is semi-tranparent. This is exactly what we want:
[/size]

There’s one thing thought that the article above makes clear: since we have rounded corners, we wouldn’t want the image content go off that boundary. This problem isn’t immediately noticeable because, in the example given in the book, the author is using an image with transparent background, but if you replace it with a regular rectangular image, it will become evident:

To solve this, you have to set the CALayer maskToBounds property to YES:

However, turning this on will also clip the shadow:

So, to fix this new problem you have to put your CALayer inside another CALayer, with the exact same size as the first, and apply the shadow to this new layer instead. This way the original CALayer will clip its contents to its rounded rect bounds, but will not affect the shadow created by its superLayer.

[size=85]Changes to the code (we now have another CALayer encapsulating the first one):
[/size][code]boxLayer = [[CALayer alloc] init];
[boxLayer setBounds:CGRectMake(0, 0, 85, 85)];
[boxLayer setPosition:CGPointMake(160, 100)];

    [boxLayer setShadowOpacity:1.0];
    [boxLayer setShadowColor: [[UIColor blackColor] CGColor]];
    [boxLayer setShadowOffset:CGSizeMake(10, 10)];

    
    CALayer *box2 = [[CALayer alloc] init];
    [box2 setBounds:CGRectMake(0, 0, 85, 85)];
    [box2 setPosition:CGPointMake(42.5, 42.5)];

    [box2 setCornerRadius:[boxLayer bounds].size.width/4];
    
    UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5];
    CGColorRef cgReddish = [reddish CGColor];
    [box2 setBackgroundColor:cgReddish];
    
    UIImage *layerImage = [UIImage imageNamed:@"test.png"];
    CGImageRef image = [layerImage CGImage];
    [box2 setContents:(__bridge id)image];
    
    [box2 setMasksToBounds:YES];
    [box2 setContentsRect:CGRectMake(-0.1, -0.1, 1.2, 1.2)];
    [box2 setContentsGravity:kCAGravityResizeAspect];
    
    [boxLayer addSublayer:box2];
    [[self layer] addSublayer];[/code]

[size=85]Result:
[/size]

The last part of this puzzle was given to us by our friend Gorthmog: instead of giving our layer a semi-transparent red background, we should give it a full opaque background. Then we set it’s superLayer’s (the one that creates the shadow) opacity to anything < 1. This will create a shadow only on the outside of our content CALayer, while keeping its semi-transparent content intact :

[size=85]Code changes (also going back to the original image, now that we know we are clipping it appropriately):
[/size] [boxLayer setOpacity:0.5]; ... [box2 setBackgroundColor:[[UIColor redColor] CGColor]];

[size=85]Result:
[/size]

Notes:

@Gorthmog: Thanks for posting your solution! I tested removing the [layer setShouldRasterize:YES] and it still works perfectly, so the secret was just the full opaque background.

@pschluet: If you want the image content of your layer to respect the rounded corners, then you do need a second layer. If you are not using rounded corners, then you don’t need to clip its contents, and your solution would be correct.

Hope it helps.

Cheers!

Gilmar


#11

Very cool, Gilmar, and a pleasure to read: you’re making me wish that I had posted more screen shots with mine.

What do you think about my nit-pick that when you set layer opacity, now the Hypno.png image is partly transparent (and not just the background)?


#12

[quote=“kullyflower”]Very cool, Gilmar, and a pleasure to read: you’re making me wish that I had posted more screen shots with mine.

What do you think about my nit-pick that when you set layer opacity, now the Hypno.png image is partly transparent (and not just the background)?[/quote]

Hi Kullyflower,

I like to think from the user’s perspective of things. To the user the image “belongs” to that square thing. So, if the square (CALayer) is semi-transparent, it’s content should also be, which is exactly how Apple made it to work. I mean, it wouldn’t exactly be helpful to drink an invisibility potion, and have only my clothes to become invisible.

I do agree with you, completely, that we should pay attention to the finest details, and pursue the maximum level of polish and precision of our projects, but since our resources are limited, we have to pick our fights wisely. And it seems that you really went deep into this, and got to a pretty advanced alternative, which is great.

So, what I think we have to ask ourselves in situations like this is “would this benefit the user greatly?”. In this particular case, I don’t think it is worth it.

Cheers!

Gilmar