Browsing articles tagged with " custom"

Custom UITableViewCells loaded from XIB, howto & debug

Jan 4, 2011   //   by Romain Vincens   //   Blog  //  3 Comments

There is a lot of literature which can be found on the net about how to load UITableViewCells from XIB files, for a use in UITableViews. You can find at least 3 or 4 different methods to achieve it, and everyone is convince their method is better than other’s.

The Apple way

While I’m not interested now in debating which is best, it looks to me there’s a lot of confusion about how to achieve it right and quickly, so I am going to detail the one that Apple present as the official way.
An example is worth a thousand words, this one is taken from the Recipes example in Apple documentation:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *IngredientsCellIdentifier = @"IngredientsCell";

    EditingTableViewCell *cell = (EditingTableViewCell *)[tableView dequeueReusableCellWithIdentifier:IngredientsCellIdentifier];
    if (cell == nil) {
		[[NSBundle mainBundle] loadNibNamed:@"EditingTableViewCell" owner:self options:nil];
        cell = editingTableViewCell;
		self.editingTableViewCell = nil;
    }

    ....

}

The three interesting lines are located inside the if block. While they look strange at first, you get all the meaning once you open EditingTableViewCell.xib and see how things got laid down:

The type of the view (the class) is EditingTableViewCell. IBOutlets are mapped to that file. However, the File’s Owner is IngredientDetailViewController, which is the view controller managing the UITableView displaying the EditingTableViewCell.

Right-clicking the File’s Owner shows that EditingTableViewCell is mapped to an IBOutlet called editingTableViewCell in the controller.

Giving a quick look at IngredientDetailViewController.h shows the outlet:

@property (nonatomic, assign) IBOutlet EditingTableViewCell *editingTableViewCell;

That means that when the code calls

[[NSBundle mainBundle] loadNibNamed:@"EditingTableViewCell" owner:self options:nil];

the EditingTableViewCell XIB file is going to be loaded and attached to the self (the owner), in the editingTableViewCell ivar.

cell = editingTableViewCell;

keeps a reference to that loaded XIB, which is a EditingTableViewCell object, and then

self.editingTableViewCell = nil;

resets the outlet, so that it frees up unneeded memory. The cells will be queued and reused in the UITableView, and in case the view controller needs a new reference, it can load it again from the XIB.

Smart. Efficient.

Reusing in other view controllers

The downside is that the EditingTableViewCell XIB is associated to IngredientDetailViewController which is not nice if you want to reuse your XIB for a UITableView in another controller.

Duplicating the file is not a solution. In fact, you can load the XIB from another controller, as long as the outlet is named exactly the same. Even if it’s the original controller which is mentioned as the File’s Owner in the XIB, it works.

Debugging a blank view controller

There’s a common mistake in the class/xib setup that can lead to hours of debugging while all was going smooth a few seconds before. When creating your custom UITableViewCell XIB file, you enter the view controller’s name in the File’s Owner (4th tab in the Inspector).
Then you link the outlet to your view, but you should not link the view controller’s view to your view in that XIB. Instead it must remain blank (it will be mapped by the XIB of the view controller).

Do not link your controller's view outlet in a custom UITableViewCell's XIB file, or you will likely get bugs

Linking the view as well in the cell’s XIB will cause your view controller to appear blank when running the application. This is due to the conflict that the view controller’s view is linked in two different XIBs, and XCode doesn’t show any warning for this. You’re now warned.

Animate CALayer custom properties with CoreAnimation

Nov 26, 2010   //   by Romain Vincens   //   Blog  //  11 Comments

Apple’s Core Animation framework is a powerful way for developpers to produce animated content in their graphical interfaces. It is simple enough so that the developer only needs to specify the initial and final states of an object in order to make it animate. Core Animation handles interpolation of intermediate values and executes the animation in different thread than the main run loop, the developper doesn’t need to write specific code for the animations. And CoreAnimations can be automagically accelerated by the GPU.

CoreAnimation can be used in several ways, from the high-level UIView animation syntax down to implicit and explicit CAAnimations.
All animations finally end up being animations of CALayers properties. There are a number of CALayer properties that can be animated, they are called the animatable-properties by Apple (opacity, bounds, content, cornerRadius etc…)

However in some cases, the developper may need to implement its own CALayer subclass. The main purpose of doing such a thing is to let the developer draw custom content in the drawInContext: method (a bit like the drawRect: UIView method).

Let’s say you have a custom CALayer, which basically draws a circle with a given radius. The circle have a border on which you can control the thickness. Let’s name that class CircleLayer.

Basically its interface would look like this:

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface CircleLayer : CALayer {
	CGFloat		radius;
	CGFloat		strokeWidth;
}

@property (nonatomic, assign) CGFloat	radius;
@property (nonatomic, assign) CGFloat	strokeWidth;

@end

and in CircleLayer.m

- (void)drawInContext:(CGContextRef)ctx {
    NSLog(@"Drawing layer, strokeWidth is %f, radius is %f", self.strokeWidth, self.radius);

    CGPoint centerPoint = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2);

	/* Path the circle */
    CGContextAddArc(ctx, centerPoint.x, centerPoint.y, self.radius, 0.0, 2*M_PI, 0);
    CGContextClosePath(ctx);

    /* And fill it */
    CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextFillPath(ctx);

	/* Path the circle again */
    CGContextAddArc(ctx, centerPoint.x, centerPoint.y, self.radius, 0.0, 2*M_PI, 0);
    CGContextClosePath(ctx);

	/* Stroke the path */
	CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor);
	CGContextSetLineWidth(ctx, self.strokeWidth);
	CGContextStrokePath(ctx);
}

If the developper want to make the radius and/or the stroke width of the circle to animate, he would implement a CAAnimation with a fromValue and toValue:

- (void) animateRadius {
	CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"radius"];
	anim.duration = 3.0;
	anim.fromValue = [NSNumber numberWithDouble:50.0];
	anim.toValue = [NSNumber numberWithDouble:150.0];
	anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

	[circleLayer addAnimation:anim forKey:@"animateRadius"];

	circleLayer.radius = 150.0;
}

- (void) animateStrokeWidth {
	CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"strokeWidth"];
	anim.duration = 4.0;
	anim.fromValue = [NSNumber numberWithDouble:25.0];
	anim.toValue = [NSNumber numberWithDouble:1.0];
	anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

	[circleLayer addAnimation:anim forKey:@"animateStrokeWidth"];

	circleLayer.strokeWidth = 1.0;
}

However these two methods (which would be placed for instance in the UIView hosting the circleLayer) attempt to animate radius and strokeWidth which are not part of CoreAnimation’s animatable properties. So they wont animate.

But there is a solution, which is not really clear to me in Apple’s documentation. CircleLayer.m needs to implement two other methods:

The first, needsDisplayForKey: tells CoreAnimation which properties of the layer causes the layer to be marked as ‘dirty’ (i.e: needs to be redrawn by drawInContext: method).

+ (BOOL)needsDisplayForKey:(NSString *)key {
	if ([key isEqualToString:@"radius"]
		|| [key isEqualToString:@"strokeWidth"]) {
        return YES;
    }
	else {
        return [super needsDisplayForKey:key];
    }
}

So here we tell that an update on radius or on strokeWidth should cause the layer to be redrawn (other properties cause the redraw too, with the help of [super needsDisplayForKey:key]).

The second, initWithLayer: will ensure that custom properties will be copied for presentation layers. When you animate a CALayer, it creates presentation copies of the layer (using initWithLayer:) for each and every frame of the animation. Each presentation layer contains an intermediate value of the animated properties. The original layer contains the final state of the properties.

- (id) initWithLayer:(id)layer {
	if(self = [super initWithLayer:layer]) {
		if([layer isKindOfClass:[CircleLayer class]]) {
			CircleLayer *other = (CircleLayer*)layer;
			self.radius = other.radius;
			self.strokeWidth = other.strokeWidth;
		}
	}
	return self;
}

Since our custom properties are not copied automatically by CALayer, not implementing initWithLayer: would result in radius and strokeWidth to be zeroed for each intermediate frame, and then finally jumping to their last value when the last frame is displayed.

The radius and border thickness currently being animated

Now you have a simple solution to animate whatever kind of properties on your custom CALayer as long as they represent numbers (which can be interpolated). And yes it works with NSNumbers too.

[UPDATE]: You can find a demo project in this article.

Custom HTTP headers for every request made in UIWebViews

Sep 9, 2010   //   by Romain Vincens   //   Blog  //  7 Comments

NSMutableURLRequest are handy classes for making HTTP requests. They allow you to add custom HTTP headers before sending a request. And it works fine.
However in a project, I have a webview displaying a webpage hosted on a server (I know it sucks but that’s not the point). The content of the webpage had to be slightly different depending on the version of the iPhone app displaying the page.
Until today the version of the page was always the same, whatever the application version. But today, to differentiate older version of the app with newer, we decided to add a custom HTTP header to the initial webview request, something like “app-version:1.1″.
However it turned out that every single link tapped in the webpage is handled by the UIWebView itself and obviously the UIWebView is not adding the new HTTP header furthermore. And we needed the HTTP header to be present in every request made by the webview to the webserver.

So the solution I brought to the problem was to override the default UIWebView. My new NPWebView class inherits UIWebView AND is delegate of the UIWebView. So that it implements the webView:shouldStartLoadWithRequest:navigationType: delegate selector.

First version of the NPWebView class asserted that the passed NSURLRequest was a NSMutableURLRequest and then added a HTTP header to it. And it worked fine!
Until I moved the project to OS4.0, where the trick didn’t work anymore. It seems Apple has modified their implementation of the UIWebview, making any change to the NSURLRequest within the webView:shouldStartLoadWithRequest:navigationType: selector inoperant. So I implemented a new workaround, that works pretty well and should now work for whatever version of the OS exists or will exist in the future.

The new solution is less efficient, I admit it. If you have a better solution, don’t hesitate to drop a comment!
In the webView:shouldStartLoadWithRequest:navigationType: selector, we’re looking in the given request for the HTTP header we want to add, see if it’s there.

  • If not, then we copy the request, discard the given one (return NO), add the custom HTTP headers to the copied request and then ask self to load that request.
  • If present, then we let load the request (return YES).

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)aRequest navigationType:(UIWebViewNavigationType)navigationType {

	NSDictionary *headers = [aRequest allHTTPHeaderFields];
	BOOL hasWhateverAddedHeader = NO;
	for(NSString *key in [headers allKeys]) {
		if([[key lowercaseString] isEqualToString:@"my-added-header"]) {
			hasWhateverAddedHeader = YES;
			break;
		}
	}
	if(!hasWhateverAddedHeader) {
		NSMutableURLRequest *newRequest = [aRequest mutableCopy];
		[newRequest addValue:@"whatever" forHTTPHeaderField:@"My-Added-Header"];
		[self addAppVersionHTTPHeader:newRequest];
		[self loadRequest:newRequest];
		[newRequest release];
		return NO;
	}
	return YES;
}

But one can argue that my NPWebView class is already the delegate of the UIWebView, thus not allowing any other class to be delegate of the original webview. The answer I brought was to reimplement the setDelegate: selector which will retain another delegate while keeping the true delegate as being the NPWebView class.

The NPWebView class I wrote has more features, like the ability to set a NSDictionary for custom headers and solves the delegate issue.

Hit the link to download a sample project with the NPWebView class (work under Apache License version 2.0).