Challanges


#1

Ok so i’ve managed to do the first challenge but not the second one.
Am i correct that the functions i need to use are those in Quartz or is it a UIKit way of doing this more easily? The thing is that i really don’t feel for reading in Quartz before i got a good understanding of UIKit. Or is it recommended to read up on Quartz it at this point?


#2

Hi,

I’ve just reached Chapter 24 and (so far :astonished: ) this challenge took me the longest by far.

I would love to see the text book answer to this one as I suspect there are much better ways of doing it.

I ended up using a path to draw rounded corners and then clip (CGContextClip) before drawing the image.
The gradient was a bit of trial and error - kept plugging different values until it looked half way reasonable.
The biggest stumbling block though was managing the different orientations and it look me a while to realise how rotation and the coordinate system works.

I read the Quartz 2D Programming guide at least twice - some parts three times !

I also found it useful to knock up a simple image viewer to avoid navigating through the Homepwner interface to see the results.

Good luck.
Gareth


#3

Hi,

I was able to come up with a simple way to do the rounded corners, using the CALayer’s maskToBounds and cornerRadius properties. Something like this:

@implementation HomepwnerItemCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
        ...
        imageView = [[UIView alloc] initWithFrame:CGRectZero];
        // Tell the imageview to resize its image to fit
        [imageView setContentMode:UIViewContentModeScaleAspectFit];

        imageView.layer.masksToBounds = YES;
        imageView.layer.cornerRadius = 8.0;

        imageSubView = [[UIImageView alloc] initWithFrame:CGRectZero];
        [imageView addSubview:imageSubView];
        [imageSubView release];
        ...

I converted imageView to a regular UIView, and added imageSubView, which is the UIImageView where you add the actual image.

- (void) layoutSubviews
{
    ...
    [imageView setFrame:innerFrame];
    [imageSubView setFrame:imageView.bounds];
    ...

Maybe that is simpler than using the CGContextClip?

Anyway, now I am trying to do the “nice glossy gradient”, and I am stuck. I’m trying to do it with the CAGradientLayer (since layers seemed to work so nicely above). Here is a snippet of my code:

- (void) layoutSubviews
{
    ...
    CAGradientLayer *gradient = [CAGradientLayer layer];
    gradient.frame = imageView.bounds;
    gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor blackColor] CGColor], (id)[[UIColor whiteColor] CGColor], nil];
    [imageView.layer insertSublayer:gradient atIndex:0];
    ...

When I try this, I see a nice black->white gradient on the cells that have no thumbnail, but the ones with thumbnails look unchanged (there is no gradient). Has anyone else tried this approach?

-Chris


#4

Hi,

Have you tried changing the opacity of the layers to see if they are both being displayed?

It sounds like the image is masking the gradient.

Gareth


#5

Yep, you were correct. The opacity was messing me up. I ended up inserting the CAGradientLayer at index 1 (so it would be on top), and then decreasing its opacity. Looks beautiful. Thanks for the tip!

-Chris


#6

I have got two questions, since I kind of managed the challenge but I am not too happy about it. My approach is not exactly the same as Chris’s, but the principle is the same.
Add an gradient layer to an UIImageView.

  1. I see that I did not used the imageSubView at all. I do only use the imageView. I even change my code the way Chris did but with the same effect.
  2. I have noticed that I have to add the CAGradientLayer twice in the order to get right opacity-gradient effect.

   [imageView.layer insertSublayer:grdLayer atIndex:[[[imageView layer] sublayers] count]];            

As mentioned earlier, it somehow works OK, but there has been too much “cut and try” and I would like to understand why.

Any comments ?

/petron


#7

I agree that this has been the hardest challenge so far - I think Joe mentioned somewhere that with Cocoa you can solve most things with just a couple of lines of code - but it seems it is not always obvious what the two lines of code are and exactly where you should put them.

My solution is very similar to Chris’ but I have a couple of questions.

  1. What’s the best way to get rid over the gradient over nothing when there isn’t a thumbnail? I subclassed HomepwnerItemCell to HpItemCellThumb and put all the gradient and corner rounding code in the layoutSubviews methods of the subclass. I then changed the tableView:cellForRowAtIndexPath: in ItemsViewController to create two different cell types based on whether imageKey exists or not. Is this overkill - is there a simpler way to achieve this?

  2. What exactly happens when you cast something (say a CGColorRef) to an id? The only way I could get the colours into the NSArray that setColors: needs was to use something like (id)startColorRef. Are there any memory implications to this?

Anthony.


#8

Hi,

I’ve got a theory on petron’s situation…

put some NSLogs showing [[[imageView layer] sublayers] count] where you insert the gradient subLayer.

I suspect that if you click on the detail and return a few times (to trigger layoutSubViews) you’ll see the count increasing each time.

If that’s the case then what is happening is, each time around, you are adding an additional gradient on top of the image. Its probably getting a bit darker each time !

You could go for Chris’s approach and have a separate imageSubView that is placed on top of the image (once) or I guess put an if statement around your code to only add a gradient if the count is 0 (feels a bit clunky)

…or you could manipulate the image when it is first saved and lose a few days reading about coordinate systems, paths, contexts, transforms etc and wish you’d known about CALayers maskToBounds and cornerRadius properties :smiley:

HTH
Gareth


#9

Hi Gareth,
Yes you are right I do have an if statement that blocks from adding too many gradient layers since as you noticed, every time you redraw the screen a new gradient layer is added and the picture gets darker for every time.

I even noticed that I do have to call it twice to get the proper effect.

I probably have to consider to look at Chris solution to see if it will help to add a separate subView, but when looking at his code he do “insertSubLayer” to the imageView every time the layoutSubview is called, it means he suppose to see the same problem.

Thanks for comments.

/petron


#10

Hi all,

Just some notes about my implementation:

  • I needed to add the “imageSubView” to get the rounded corners to look correct for the thumbnail. The corners just didn’t look right when the thumbnail image was directly contained by the imageView. It was like they were getting clipped off. Maybe it has something to do with the frame?

  • I moved the code that adds the CAGradientLayer out of layoutSubviews, and into initWithStyle. That’s probably why I don’t see the problem with the thumbnail getting darker & darker. Now, in layoutSubView I just set the frame size for the gradientLayer.

  • I always add a CAGradientLayer, regardless of whether there is a thumbnail or not. However, in my setPossession, I set the “hidden” property on the gradientLayer if there isn’t a thumbnail. So it’s there, just hidden.

My biggest problem is the crash that I’m seeing, related to low-memory & the imageCache. Here’s the post:
http://forums.bignerdranch.com/viewtopic.php?f=57&t=276
Has anyone else seen this crash?

-Chris


#11

Hi Petron,

I’m not sure at all that this is the best way but if I take the solution “Homepwner_TableViewCell” direct from this website.

and add the following to the end of layoutSubviews

if (imageView.image && [imageView.layer.sublayers count] == 0)
	{
		CAGradientLayer *gradient = [CAGradientLayer layer];
		gradient.frame = imageView.bounds;
		gradient.opacity = 0.2;  // or whatever
		gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor blackColor] CGColor], (id)[[UIColor whiteColor] CGColor], nil];
		[imageView.layer insertSublayer:gradient atIndex:0];
	}

then it seems to have the desired effect.

If that doesn’t work then I guess there’s something else going on elsewhere.

Gareth


#12

Since I was interested in trying Core Graphics I tried to find a solution for the last challenge along that path. It took me a while to get everything to work as I wanted, but now I got a working solution.

When taking a picture using my iPhone 4 I noticed the raw images were huge. After taking only one picture (maybe 2) I already got memory warnings and the detailView became very slow when it (dis)appeared. I decided to resize the image before it was stored and suddenly everything worked much better. The ImageCache didn’t need to flush its cache all the time and the detailView was responsive as it should be again.

When writing the code to make the gradient I first put it in the setThumbnailDataFromImage: method in my Possession class. The big disadvantage of this was that I needed to take a new picture every time I changed my drawing code. Therefor I put my code into the thumbnail: method so I could save the raw (though already resized) image data. Every time I restarted the application, I would recreate the thumbnail UIImage from the thumbnailData using the new drawing code. However, since I need to restart the App anyway when I need to test new code, the thumbnail UIImage is retained until shutdown of the App. I suppose it would make sense to move the code in a production App back to the setThumbnailDataFromImage: method.

Below is my thumbnail: method. First I create a CG context with a clipping path to create the rounded corners. Then, I draw the original image into that context. After that, I create a gradient and draw that into a second image. I suppose I could draw directly to the first context, but in that case I’d lose the flexibility of using BlendModes. Then, I draw the gradient image into the first context. Finally, I convert back to a UIImage, clean up the CG-pointers I used and return the brand new thumbnail. The only challenge that’s left is to make it really look 3d, using this gradient appears a little dull as opposed to the ‘nice glossy gradient’ described in the book. :wink:

[code]- (UIImage *)thumbnail
{
// Am I imageless
if (!thumbnailData) {
return nil;
}

// Delete thumbnail if image has been deleted
if (imageKey == nil) {
	[thumbnailData release];
	thumbnailData = nil;
	return nil;
}

// Is there no cached thumbnail image?
if (!thumbnail) {
	// Radius for rounded corners
	int radius = 10;
	
	// Get the CGImage data for the thumbnail
	CGImageRef imageRef = [[UIImage imageWithData:thumbnailData] CGImage];
	
	int w = CGImageGetWidth(imageRef);
	int h = CGImageGetHeight(imageRef);
	CGRect imageRect = CGRectMake(0, 0, w, h);
	
	// Init a graphics context with an alpha layer
	CGContextRef contextRef = CGBitmapContextCreate(NULL, 
													w, 
													h, 
													8,
													(4 * w), 
													CGImageGetColorSpace(imageRef), 
													kCGImageAlphaPremultipliedLast);
	
	// Create a path for rounded corners
	CGContextBeginPath(contextRef);
	CGContextMoveToPoint(contextRef, 0, radius);
	CGContextAddArc(contextRef, radius, radius, radius, M_PI, 1.5 * M_PI, 0);
	CGContextAddLineToPoint(contextRef, w-radius, 0);
	CGContextAddArc(contextRef, w-radius, radius, radius, 1.5 * M_PI, 0, 0);
	CGContextAddLineToPoint(contextRef, w, h-radius);
	CGContextAddArc(contextRef, w-radius, h-radius, radius, 0, 0.5 * M_PI, 0);
	CGContextAddLineToPoint(contextRef, radius, h);
	CGContextAddArc(contextRef, radius, h-radius, radius, 0.5 * M_PI, M_PI, 0);
	CGContextClosePath(contextRef);
	
	// Use the path to set clipping area for the context
	CGContextClip(contextRef);
	
	// Draw the thumbnail in the context
	CGContextDrawImage(contextRef, imageRect, imageRef);
	
	// Create a new context for the gradient
	CGContextRef gradientContext = CGBitmapContextCreate(NULL, 
														 w, 
														 h, 
														 8, 
														 (4*w), 
														 CGImageGetColorSpace(imageRef), 
														 kCGImageAlphaPremultipliedLast);
	// Set up gradient (axial)
	CGGradientRef myGradient;
	size_t num_locations = 2;
	CGFloat locations[2] = {0.0, 1.0};
	CGFloat components[8] = {0.0, 0.0, 0.0, 0.4, // Start color
		1.0, 1.0, 1.0, 0.4}; // End color
	myGradient = CGGradientCreateWithColorComponents(CGImageGetColorSpace(imageRef), components, locations, num_locations);
	
	// Vertical linear gradient
	CGPoint gradientStart = CGPointMake(0, h);
	CGPoint gradientEnd = CGPointMake(0, 0);
	
	// Draw gradient in gradient image context
	CGContextDrawLinearGradient(gradientContext, myGradient, gradientStart, gradientEnd, 0);
	
	CGImageRef gradientImage = CGBitmapContextCreateImage(gradientContext);
	
	// Set blend mode for original context
	CGContextSetBlendMode(contextRef, kCGBlendModeLighten);
	
	// Draw the gradient image onto the original context
	CGContextDrawImage(contextRef, imageRect, gradientImage);
	
	// Create CGImage from the image context
	CGImageRef newImageRef = CGBitmapContextCreateImage(contextRef);
	thumbnail = [UIImage imageWithCGImage:newImageRef];
	
	
	// Clean up gradient
	CGGradientRelease(myGradient);
	CGContextRelease(gradientContext);
	CGImageRelease(gradientImage);
	
	// Clean up base image
	CGContextRelease(contextRef);
	CGImageRelease(newImageRef);
	
	[thumbnail retain];
}

return thumbnail;

}[/code]


#13

All righty, since I was stuck on this challenge like lots of people seem to have been, I decided to try a variant of OneTon’s implementation below. After getting a nice thumbnail of a linear gradient but no underlying image using snippets of OneTon’s code, I threw up my hands in disgust, returned my code to the original implementation, and then copied, verbatim, all of OneTon’s code below starting at if (!thumbnail) {

Now I get a nice thumbnail with rounded corners, but no gradient overlay at all. I even tried changing the blend style to “normal” and increasing the alpha value to 1.0.

So I am, once again, stuck.

Any thoughts? Clearly, the code works for OneTon, so I’m a bit frustrated.


#14

If you keep the code, but create the thumbnail from the gradientImage instead of the contextRef, do you get a gradient thumbnail? In that case something goes wrong in the blending of the 2 images.

My solution for getting the colorSpace might not have been the best one, maybe creating a Device RGB colorspace and using it in creating the 2 contextRefs will work: (this is just a guess though)

[code]CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

//…
CGContextRef gradientContext = CGBitmapContextCreate(NULL,
w,
h,
8,
(4w),
colorSpace,
kCGImageAlphaPremultipliedLast);
//…
CGContextRef gradientContext = CGBitmapContextCreate(NULL,
w,
h,
8,
(4
w),
colorSpace,
kCGImageAlphaPremultipliedLast);
//…
myGradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, num_locations);
//…
//Don’t forget to release
CGColorSpaceRelease(colorSpace);[/code]

I can’t think of anything else.


#15

The issue I was having is the code for adding the gradient to the thumbnail never got executed, because thumbnail was never equal to nil in my implementation. With some tweaking and playing, (i ended up putting the aesthetic code into a new void method called “makeItPretty” to separate it out and make things more intuitive for me to work with.

Now, a thought on how to get a “nice glossy” gradient effect as described in the book - try switching the blend mode to “screen” and then play with the alpha. I’m getting some nice results. I might move the starting point of the gradient up to h/2 to make the effect more interesting, as well :wink:

[edit: oops, I should have said, “move the ending point of the gradient down to h/2 to make the effect more interesting” as I now see that the gradient effect starts at the top and ends at the bottom in the original configuration.]


#16

I think the key to making the image look 3d is to add a bezeled edge using the gradient, creating a shading effect. This would actually need two gradients, one vertical, and one horizontal.

(Now to go and play…)


#17

How do i set my CAgradientLayer’s opacity to a value? I tried setOpacity but it doesnt work.

I did this:

[code]

  • (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
    {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
    valueLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    [[self contentView] addSubview:valueLabel];
    [valueLabel release];
    nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    [[self contentView] addSubview:nameLabel];
    [nameLabel release];

      // set the imageview
      imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
      [imageView setContentMode:UIViewContentModeScaleAspectFit];
      imageView.layer.masksToBounds = YES;
      imageView.layer.cornerRadius = 8.0;
      imageSubView = [[UIImageView alloc] initWithFrame:CGRectZero];
      [imageView addSubview:imageSubView];
      [imageSubView release];
      [[self contentView] addSubview:imageView];
      [imageView release];
    

    }
    return self;
    }[/code]

and

-(void)layoutSubviews{
    [super layoutSubviews];
    
    float inset = 5.0;
    CGRect bounds = [[self contentView] bounds];
    float h = bounds.size.height;
    float w = bounds.size.width;
    float valueWidth = 40.0;
    CGRect innerFrame = CGRectMake(inset, inset, h, h-inset * 2.0);
    [imageView setFrame:innerFrame];
    [imageSubView setFrame:imageView.bounds];
    
    //Added for glossy effect and rounded pixels
    CAGradientLayer *gradient = [CAGradientLayer layer];
    gradient.frame = imageView.bounds;
    gradient.opacity = 0.2;
    // was inside gradient.colors=(id)[[UIColor colorWithWhite:1.0f alpha:0.8f] CGColor]
    gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor blackColor] CGColor], (id)[[UIColor whiteColor] CGColor], nil];
    [imageView.layer insertSublayer:gradient atIndex:1];
    
    
    innerFrame.origin.x += innerFrame.size.width + inset;
    innerFrame.size.width = w - (h + valueWidth + inset * 4);
    [nameLabel setFrame:innerFrame];
    
    innerFrame.origin.x += innerFrame.size.width + inset;
    innerFrame.size.width = valueWidth;
    [valueLabel setFrame:innerFrame];
}

but im getting a black & white gradient over the image on two old images I had already taken on my iphone before I updated the code to add the rounded corners and glossy gradient effects. However, on a new picture I took on the iphone after that upgrade, the normal picture shows up but without and glossy gradient effect, only the rounded corners.

One more thing, each time I load the detailVC and return to the listVC, the thumbnail gets darker and darker, like as if the gradient is growing and growing over again each time I move in & out of the detailVC.