WWDC: iPhone App DescriptionsWWDC: Skittles dispensedCocoaHeads at WWDCWWDC: DoubleTwist hacks the Apple Store

fill the void

Custom UITableViewCell from a XIB in Interface Builder

Looking around the App Store, I see most apps customize their UITableViews in a unique way. Flixster embeds movie posters and ratings, in addition to their titles. Tweetie integrates tweets, icons, usernames, and the date. GasBuddy lists service type, amount spent, gallons, and dollars per gallon in each row. Constructing these customized UITableViewCells is possible in code, but leveraging Interface Builder’s drag-and-drop interface is far more fun. Thanks to Bill Dudney for talking about one approach to this on his blog and to StackOverflow for covering this topic.

Creating a custom UITableViewCell using Interface Builder is straight-forward.

  1. In Xcode, create a new UITableViewCell subclass and add the desired IBOutlets to the header file.
  2. In Interface Builder, create an “Empty” XIB from the Cocoa Touch palette.
  3. Drag a UITableViewCell from the Library into it, configure the class to be your new custom UITableViewCell subclass, and give it the appropriate identifier.
  4. Add the desired elements to the UITableViewCell and connect the subclass’s outlets to them.
  5. To instantiate the cells using the UIViewController approach, set class of the new XIB’s “File’s Owner” to be “UIViewController”, and connect its view outlet to the customized UITableViewCell.

The XIB is set up, but there are two approaches to instantiating the new customized cell from that XIB. When I was at WWDC a couple weeks ago, I confirmed with one of the Interface Builder engineers at the IB Lab that both work just fine. (He did repeatedly ask if I was using UITableView:dequeueReusableCellWithIdentifier:, just to make sure.)

UIViewController

One approach is to create a temporary UIViewController each time you need a new cell. By setting up the XIB the way we did, the temporary UIViewController has the cell as its view attribute. After we grab a pointer to that, we can release the view controller.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
		// Create a temporary UIViewController to instantiate the custom cell.
        UIViewController *temporaryController = [[UIViewController alloc] initWithNibName:@"BDCustomCell" bundle:nil];
		// Grab a pointer to the custom cell.
        cell = (BDCustomCell *)temporaryController.view;
		// Release the temporary UIViewController.
        [temporaryController release];
    }

    return cell;
}

NSBundle:loadNibNamed:owner:options:

Another approach is to load the NIB file and grab the cell directly, as it’s the only top-level object in the NIB.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BDCustomCell"];
    if (cell == nil) {
		// Load the top-level objects from the custom cell XIB.
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BDCustomCell" owner:self options:nil];
		// Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
		cell = [topLevelObjects objectAtIndex:0];
    }

    return cell;
}

ibtool Caveats

I’ve previously covered how Apple’s command-line utility ibtool can be used to automate localization tasks: generating a .strings file, localizing an English XIB, and incrementally updating a localized XIB. It’s a fantastic tool, included in the Xcode development installation, and works well for both Mac OS X applications and iPhone apps. I use this automation extensively on Mac OS X (10.5): 14 XIBs across 11 languages means many, many files to keep in sync. However, the heavy use has exposed an annoying bug in ibtool: silent errors.

“Could not be parsed”

When ibtool encounters an expected bad character, like an unescaped double-quote, it verbally fails, supplying the message below.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.ibtool.errors</key>
	<array>
		<dict>
			<key>description</key>
			<string>ibtool failed with exception: The stringsfile MainMenu.ES.strings could not be parsed.</string>
		</dict>
	</array>
</dict>
</plist>

This error is helpful, but it doesn’t identify the offending line. To do that, we can use another Apple command-line tool: plutil. (Thanks to Cocoa Musings for pointing this out.) plutil can parse a .strings file and provide the line the bad character was on. It’s a very useful program when the .strings file that failed has 3K lines, and the problem is a single unescaped double-quote. ibtool generates these verbal parse errors on all alphanumeric characters and a number of others (” ‘ : . / - _ $).

Silent Errors

Unfortunately, ibtool (and plutil) fails silently on these other characters:

{ } [ ] ; , < > ? \ | = + ! ` ~ @ # % ^ & * ( )

Let me say that again: ibtool fails silently. The program stops incorporating localized strings when it encounters one of these characters, but it does not produce an error. The bug becomes particularly nasty when scaling up to 150 localized XIBs. I only discovered the bug when I found a single XIB incorrectly localized and tracked it back to a set of brackets randomly inserted into a .strings file.

So, I wrote a ruby script, validate_strings_files.rb, to validate .strings files more completely. The script parses through a directory of .strings files and produces verbal errors, with line numbers, for any out-of-place characters. It works well on the 11 languages I deal with. An error that would silently fail in ibtool would produce the following result with the script:

VERIFYING FILE MainMenu.ES.strings
MainMenu.ES.strings (328): @

Below is the code for validate_strings_files.rb.

#!/usr/local/bin/ruby
# encoding: UTF-8

# NOTE: requires ruby 1.9
# MIT license

# validate_strings_files.rb
# This script reads through the .strings files in a directory and identifies any bad characters inside,
# both those that ibtool would catch and those that ibtool would miss.
# * ibtool verbal errors: " ' : . / - _ $
# * ibtool silent errors: { } [ ] ; , < > ? \ | = + ! ` ~ @ # % ^ & * ( )

require 'FileUtils'

# Check for arguments.
if ARGV.length != 1
  puts "Usage: ruby validate_strings_files.rb path_to_strings"
  exit
end

# Get path argument and 'cd' to that path.
PATH = ARGV[0]
FileUtils.cd(PATH)

def verify_file(file)
  line_number = 0
  is_multi_line_comment = false

  # Use general method unless file encoding is UTF-16.
  file_handle = File.new(file, "r")
  # file_handle = File.new(file, "r:UTF-16LE:UTF-8") (See http://blog.grayproductions.net/articles/ruby_19s_three_default_encodings)

  puts "VERIFYING FILE #{file}"
  while (!file_handle.eof?)
    has_seen_equals_sign = false
    has_seen_semi_colon = false
    is_single_line_comment = false
    is_string = false
    line_number += 1
    previous_char = nil

    line = file_handle.readline
    # Use each_char (rather than each_byte) to support unicode.
    line.each_char do |char|
      # Notes:
      # * line can only be 2 strings, N multi-line comments, 1 equals sign (=), 1 semi-colon (;)
      # * ignore character if it's in a string
      # * single-line comments are ended by \n
      # * multi-line comments are ended by */

      if is_string || is_single_line_comment || is_multi_line_comment
        if is_string && previous_char != "\\" && char == "\""
          is_string = false
        elsif is_single_line_comment && char == "\n"
          is_single_line_comment = false
        elsif is_multi_line_comment && previous_char == "*" && char == "/"
          is_multi_line_comment = false
        end
        # Ignore.
      elsif previous_char == nil && char == "/"
        # Ignore.
      elsif previous_char == "/" && char == "/"
        is_single_line_comment = true
      elsif previous_char == "/" && char == "*"
        is_multi_line_comment = true
      elsif !has_seen_equals_sign && char == "="
        has_seen_equals_sign = true
      elsif !has_seen_semi_colon && char == ";"
        has_seen_semi_colon = true
      elsif char == " " ||char == "\n"  || char == "\r"
        # Ignore.
      elsif char == "\""
        is_string = true
      elsif char == "\000"
        # Ignore unicode padding.
      elsif line_number == 1 && previous_char == nil && char == "\377"
        # Ignore unicode file header.
      elsif line_number == 1 && previous_char == nil && char == "\376"
        # Ignore unicode file header.
      else
        puts "#{file} (#{line_number}): #{char}"
      end

      if char != "\000" && char != "\377" && char != "\376"
        # Save previous character if it's not unicode padding.
        previous_char = char
      end
    end
  end
end

# Iterate through the current directory.
Dir.entries(".").each do |file|
  filename = file.slice(0,file.length-8)
  extension = file.slice(file.length-8,file.length)
  # Only deal with .strings.
  if (extension == ".strings")
    # Read the file and identify any bad characters.
    verify_file(file)
  end
end

One important sidenote is Unicode support. The script works with Unicode characters but not universally. I have two lines to allow complete Unicode support. The first works on most languages, and the second is used for UTF-16 languages, like Japanese (JA).

# Use general method unless file encoding is UTF-16.
file_handle = File.new(file, "r")
# file_handle = File.new(file, "r:UTF-16LE:UTF-8")

Keep in mind that Ruby 1.9 is required because of its Unicode support; refer to the Hivelogic article for compilation instructions. The r:UTF-16LE:UTF-8 code is from this article on Ruby encodings.

“Class mismatch”

One other error I’ve encountered is “class mismatch”. ibtool produces this error when the class of a UI element associated with a specific ObjectID changes. For instance, imagine two languages were modified independently, and both had a UI element added to them. The first might be an NSTextField whereas the second might be an NSMenuItem; however, the IDs might be the same. The two entries in the .strings files would look like so:

/* Class = “NSTextField”; title = “First Text”; ObjectID = “419″; */
/* Class = “NSMenuItem”; title = “Second Text”; ObjectID = “419″; */

Using the first as the base for localization, incoporating the strings into the second wouldn’t make any sense for this UI element. Luckily, ibtool produces the following error when trying to localize those XIBs:

<plist version="1.0">
<dict>
  <key>com.apple.ibtool.errors</key>
  <array>
    <dict>
      <key>description</key>
      <string>Class mismatch during incremental localization for Object ID: 419.  New Base has class: NSTextField.  Old Base has class: NSTextField.  Old Loc has class: NSMenuItem.</string>
    </dict>
  </array>
</dict>
</plist>

WWDC 2009

WWDC 2009 is next week, June 8-12, in San Francisco. I’ll be there, tweeting intermittently. :)

If you’re going/around, be sure to check out CocoaHeads on Wednesday night at the local Apple Store. Scott Stevenson et al have an awesome lineup of people talking: Delicious Monster, Sofa, 280 North, and Sebastiaan de With (the guy behind Cocoia and Classics). Not to be missed.


Core Animation on the iPhone

Core Animation makes the iPhone awesome. All the nice visual cues that Apple’s built-in apps gives users are courtesy of Core Animation. How apps jiggle around when I’m moving them. How moving messages in Mail involves a flying message icon and a satisfying blue pulse from the selected folder. How terminated apps shrink into nothingness in the middle of the screen. All thanks to Core Animation. It enables developers to easily provide direct manipulation (one of Ben Schneiderman’s many excellent contributions) to their applications.

Following on my last post, Core Animation on the Mac, I ported the animation code to the iPhone.




In this case, I mocked up the view in Mail where the user moves a message to another folder. There is a bar along the top for the icon (a Star Trek one from the Iconfactory) and a short description. Below it is a UITableView list of items. When the user clicks on an item, the icon flies into the item, and then the item pulses. Instead of duplicating Mail’s blue pulse, I reused the Yellow Fade Technique from the last post.

Icon Animation

The icon animation code is almost the same as the Mac version. Instead of driving the animation through an NSView, I control it through a CALayer; I add animation objects to it keyed on its position and then assign it a new position.

/*
 * Icon animation
 */

// Determine the animation's path.
CGPoint startPoint = CGPointMake(buttonFrame.origin.x + buttonFrame.size.width / 2, buttonFrame.origin.y + buttonFrame.size.height / 2);
CGPoint curvePoint1 = CGPointMake(startPoint.x + 250, startPoint.y);
CGPoint endPoint = CGPointMake(cellFrame.origin.x + 20, cellFrame.origin.y + topView.frame.size.height);
CGPoint curvePoint2 = CGPointMake(endPoint.x + 100, endPoint.y - 100);

// Create the animation's path.
CGPathRef path = NULL;
CGMutablePathRef mutablepath = CGPathCreateMutable();
CGPathMoveToPoint(mutablepath, NULL, startPoint.x, startPoint.y);
CGPathAddCurveToPoint(mutablepath, NULL, curvePoint1.x, curvePoint1.y,
					  curvePoint2.x, curvePoint2.y,
					  endPoint.x, endPoint.y);
path = CGPathCreateCopy(mutablepath);
CGPathRelease(mutablepath);

// Create animated icon view.
[animatedIconView release];
animatedIconView = [[UIImageView alloc] init];
[animatedIconView setImage:icon];
[animatedIconView setFrame: CGRectMake(startPoint.x, startPoint.y, 32, 32)];
[animatedIconView setHidden:NO];
[enclosingView addSubview:animatedIconView];
CALayer *iconViewLayer = animatedIconView.layer;

CAKeyframeAnimation *animatedIconAnimation = [CAKeyframeAnimation animationWithKeyPath: @"position"];
animatedIconAnimation.duration = 1.0;
animatedIconAnimation.delegate = self;
animatedIconAnimation.path = path;
animatedIconAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[iconViewLayer addAnimation:animatedIconAnimation forKey:@"animateIcon"];

// Start the icon animation.
[iconViewLayer setPosition:CGPointMake(endPoint.x, endPoint.y)];

Yellow Fade Technique

The yellow fade animation is very different. There is no NSGraphicsContext on the iPhone. Instead of re-implementing drawLayer:, I simply give the NSView the appropriate properties and change its alpha value through animation code. The new code is much shorter and takes advantage of Core Animation’s repeat and auto-reverse methods.

/*
 * Yellow fade animation
 */

// Create the yellow fade layer.
[yellowFadeView release];
yellowFadeView = [[UIView alloc] init];
[yellowFadeView setFrame:cellFrame];
[yellowFadeView setHidden:NO];
[yellowFadeView setBackgroundColor:[[UIColor yellowColor] colorWithAlphaComponent:0.5]];
[yellowFadeView setAlpha:0.0];
[list addSubview:yellowFadeView];

// Create the yellow fade animation.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelay:1.0];
[UIView setAnimationDuration:.25];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationRepeatCount:2];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
[yellowFadeView setAlpha:1.0];
[UIView commitAnimations];

As with the Mac implementation, this sample project (Mac OS X 10.5, Xcode 3.1.2) is available at BDCoreAnimation_iPhone on Google Code.


Core Animation on the Mac

Core Animation is a fantastic tool in the Cocoa toolkit. It allows developers to add smooth visual animation to their Mac applications. I started playing with Core Animation after listening to Scotty’s Late Night Cocoa podcast, part of the Mac Developer Network, specifically the episode with Bill Dudney. Check out his book, Core Animation for Mac OS X and the iPhone, at Pragmatic Programmers. And be sure to look at the Mac Developer Network; Scotty’s podcasts are awesome.

While Core Animation is generally amazing, it’s especially helpful for giving the user feedback for their actions. Clicking a button can now easily show an animation that tells the user what the button is doing. To illustrate this, I’m going to walk through two simple animations: a flying icon and a yellow fade. Below is a Quicktime movie showing the two animations, and at the bottom of the post is the code behind them.




First, I setup the skeleton application. I have a source list on the left with a couple entries and child entries. I hooked this view up with a simple NSOutlineView and an NSTreeController. On the far right, there is a button to start the animation. See the screenshot below.

Icon Animation

Clicking the button fires the selector animateButton:. That method does a couple things. First, it figures out where the button is and where it wants the icon to go, including the two curve points for the bezier path. Next, it calculates the actual bezier path the icon will follow. Then, it defines the animation, setting a duration and supplying the delegate and the path. Finally, it assigns that animation to the relevant NSView and starts the animation by calling setFrameOrigin:. The one important subtlety that is in [CAKeyframeAnimation animationWithKeyPath: @"frameOrigin"];. I found Core Animation to be very finicky about what key path I used to associate the animation with the view; frameOrigin works consistently, so try it if other key paths don’t seem to work.

- (IBAction)animateButton:(id)sender {
	// Get the relevant frames.
	NSView *enclosingView = [[[NSApplication sharedApplication] mainWindow] contentView];
	int rowIndex = [sourceList selectedRow];
	NSRect cellFrame = [sourceList frameOfCellAtColumn:0 row:rowIndex];
	NSRect buttonFrame = [button frame];
	NSRect mainViewFrame = [enclosingView frame];

	/*
	 * Icon animation
	 */

	// Determine the animation's path.
	NSPoint startPoint = NSMakePoint(buttonFrame.origin.x + buttonFrame.size.width / 4, buttonFrame.origin.y + buttonFrame.size.height / 4);
	NSPoint curvePoint1 = NSMakePoint(startPoint.x, startPoint.y + 100);
	NSPoint endPoint = NSMakePoint(cellFrame.origin.x, mainViewFrame.size.height - cellFrame.origin.y - cellFrame.size.height);
	NSPoint curvePoint2 = NSMakePoint(endPoint.x + 200, endPoint.y);

	// Create the animation's path.
	CGPathRef path = NULL;
	CGMutablePathRef mutablepath = CGPathCreateMutable();
	CGPathMoveToPoint(mutablepath, NULL, startPoint.x, startPoint.y);
	CGPathAddCurveToPoint(mutablepath, NULL, curvePoint1.x, curvePoint1.y,
						  curvePoint2.x, curvePoint2.y,
						  endPoint.x, endPoint.y);
	path = CGPathCreateCopy(mutablepath);
	CGPathRelease(mutablepath);

	// Create animated icon view.
	NSImage *icon = [button image];
	[animatedIconView release];
	animatedIconView = [[NSImageView alloc] init];
	[animatedIconView setImage:icon];
	[animatedIconView setFrame:NSMakeRect(startPoint.x, startPoint.y, 20, 20)];
	[animatedIconView setHidden:NO];
	[enclosingView addSubview:animatedIconView];

	// Create icon animation.
	CAKeyframeAnimation *animatedIconAnimation = [CAKeyframeAnimation animationWithKeyPath: @"frameOrigin"];
	animatedIconAnimation.duration = 1.0;
	animatedIconAnimation.delegate = self;
	animatedIconAnimation.path = path;
	animatedIconAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
	[animatedIconView setAnimations:[NSDictionary dictionaryWithObject:animatedIconAnimation forKey:@"frameOrigin"]];

	// Start the icon animation.
	[[animatedIconView animator] setFrameOrigin:endPoint];
}

Yellow Fade Technique

A couple years ago, 37 Signals popularized a user feedback animation called the Yellow Fade Technique. When a user performs an action or the (web) application updates, the new information’s background flashes yellow and then fades away.

Clicking the button fires the selector animateButton:. This time, that method does a few different things. First, it creates an NSView and gives it a CALayer; the delegate is set to the controller, so that the controller can draw the layer. Next, it defines the animation. This animation is a bit more complicated. It’s actually a group of animations. Since I want the yellow overlay to flash twice, I define four animations: fade in and out once then fade in and out again. Perhaps the code could be tighter, but this approach works. Finally, I assign the animation group to the NSView and start it with an NSView call to setFrame:. Notice that I use frameOrigin for the animation’s key path, even though I start the animation with setFrame:. Again, Core Animation is finicky.

I have an additional method defined for drawing the layer’s content: drawLayer:. In this method, I draw a dark yellow curved rectangle around the cell frame and then fill the cell frame with a lighter yellow.

- (IBAction)animateButton:(id)sender {
	// Get the relevant frames.
	NSView *enclosingView = [[[NSApplication sharedApplication] mainWindow] contentView];
	int rowIndex = [sourceList selectedRow];
	NSRect cellFrame = [sourceList frameOfCellAtColumn:0 row:rowIndex];
	NSRect buttonFrame = [button frame];
	NSRect mainViewFrame = [enclosingView frame];

	/*
	 * Yellow fade animation
	 */

	// Create the yellow fade layer.
	CALayer *layer = [CALayer layer];
	[layer setDelegate:self];
	yellowFadeView = [[NSView alloc] init];
	[yellowFadeView setWantsLayer:YES];
	[yellowFadeView setFrame:cellFrame];
	[yellowFadeView setLayer:layer];
	[[yellowFadeView layer] setNeedsDisplay];
	[yellowFadeView setAlphaValue:0.0];
	[sourceList addSubview:yellowFadeView];

	// Create the animation pieces.
	CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation.beginTime = 1.0;
	alphaAnimation.fromValue = [NSNumber numberWithFloat: 0.0];
	alphaAnimation.toValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation.duration = 0.25;
	CABasicAnimation *alphaAnimation2 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation2.beginTime = 1.25;
	alphaAnimation2.duration = 0.25;
	alphaAnimation2.fromValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation2.toValue = [NSNumber numberWithFloat: 0.0];
	CABasicAnimation *alphaAnimation3 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation3.beginTime = 1.5;
	alphaAnimation3.duration = 0.25;
	alphaAnimation3.fromValue = [NSNumber numberWithFloat: 0.0];
	alphaAnimation3.toValue = [NSNumber numberWithFloat: 1.0];
	CABasicAnimation *alphaAnimation4 = [CABasicAnimation animationWithKeyPath: @"alphaValue"];
	alphaAnimation4.beginTime = 1.75;
	alphaAnimation4.duration = 0.25;
	alphaAnimation4.fromValue = [NSNumber numberWithFloat: 1.0];
	alphaAnimation4.toValue = [NSNumber numberWithFloat: 0.0];

	// Create the animation group.
	CAAnimationGroup *yellowFadeAnimation = [CAAnimationGroup animation];
	yellowFadeAnimation.delegate = self;
	yellowFadeAnimation.animations = [NSArray arrayWithObjects:
									   alphaAnimation, alphaAnimation2, alphaAnimation3, alphaAnimation4, nil];
	yellowFadeAnimation.duration = 2.0;
	[yellowFadeView setAnimations:[NSDictionary dictionaryWithObject:yellowFadeAnimation forKey:@"frameOrigin"]];

	// Start the yellow fade animation.
	[[yellowFadeView animator] setFrame:[yellowFadeView frame]];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
	// Bezier path radius
	int radius = 4;

	// Setup graphics context.
	NSGraphicsContext *nsGraphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:NO];
	[NSGraphicsContext saveGraphicsState];
	[NSGraphicsContext setCurrentContext:nsGraphicsContext];

	// Convert to NSRect.
	CGRect aRect = [layer frame];
	NSRect rect = NSMakeRect(aRect.origin.x, aRect.origin.y, aRect.size.width, aRect.size.height);

	// Draw dark outside line.
	[NSBezierPath setDefaultLineWidth:2];
	NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
	[[NSColor yellowColor] set];
	[highlightPath stroke];

	// Draw transparent inside fill.
	CGFloat r, g, b, a;
	[[NSColor yellowColor] getRed:&amp;r green:&amp;g blue:&amp;b alpha:&amp;a];
	NSColor *transparentYellow = [NSColor colorWithCalibratedRed:r green:g blue:b alpha:0.5];
	NSBezierPath *fillPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
	[transparentYellow set];
	[fillPath fill];

	// Finish with graphics context.
	[NSGraphicsContext restoreGraphicsState];
}

BDCoreAnimation

Combine these two animations together for a nice icon swooshing into the source list with the source list item flashing yellow when the icon finishes. Check out the project (Mac OS X 10.5, Xcode 3.1.2) at BDCoreAnimation on Google Code.

For a more in-depth tutorial on Core Animation, please check out Bill Dudney’s Core Animation for Mac OS X and the iPhone.


← Before