bdunagan
fill the void

Creating UITableView badges like iOS Mail

Apple’s iOS Mail has a universal inbox on iOS 4. Very handy in general, but its unread badges complete the feature. I frequently glance down the list of email addresses I have and know exactly what the state of my inboxes are. I never have to wonder how many new emails I have. I just check the numbers.

Recently, I needed to duplicate that feature for work. I found two existing solutions:

I ended up forking DDBadgeViewCell. It was concise, and I only needed to make a couple changes to it. Specifically, I wanted the badges to look exactly like Mail’s badges, in terms of alignment, colors, and shadows. I’ll go through my changes here, but you can check out my GitHub fork as well. See the image for the resulting look:

Badge color

I wanted to duplicate Mail’s look and feel, so I needed the exact colors that Mail uses for its badges. xScope made it easy.

UIColor *badgeColor = [UIColor colorWithRed:0.55 green:0.6 blue:0.69 alpha:1.];
UIColor *badgeShadowColor = [UIColor colorWithRed:0.45 green:0.49 blue:0.57 alpha:1.];

Badge shadow

If you look closely at Mail’s badges, you’ll notice the top part of the arc looks indented. The shadow gives the label a feeling of depth; the badge looks almost letter-pressed. To duplicate that look, I simply drew a darker oval one pixel higher than the original before drawing the real one.

// Draw the badge shadow.
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, currentBadgeShadowColor.CGColor);
CGMutablePathRef shadowPath = CGPathCreateMutable();
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.width - badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI / 2, M_PI * 3 / 2, YES);
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI * 3 / 2, M_PI / 2, YES);
CGContextAddPath(context, shadowPath);
CGContextDrawPath(context, kCGPathFill);
CFRelease(shadowPath);
CGContextRestoreGState(context);

Pixel Perfect

DDBadgeViewCell does an excellent job when there are three or more characters in the string. But as I wanted to display numbers, I needed to special-case the code a bit to get the pixels correctly aligned.

// Set up the badge's frame.
CGSize badgeTextSize = [self.cell.badgeText sizeWithFont:[UIFont boldSystemFontOfSize:18.]];
CGRect badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - badgeTextSize.width - 24, (rect.size.height - badgeTextSize.height - 4) / 2, badgeTextSize.width + 14, badgeTextSize.height + 4));
if ([self.cell.badgeText length] < 3) {
    // Fix the width for 1-2 characters.
    badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - 46, 13, 34, 25));
}

// Draw the badge shadow oval and the badge oval...

// Draw the number on the badge.
CGContextSaveGState(context);
CGContextSetBlendMode(context, kCGBlendModeClear);
if ([self.cell.badgeText length] == 1) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 11, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 2, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else if ([self.cell.badgeText length] == 2) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 6, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else {
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 7, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x, badgeTextFrame.origin.y + 1, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
    [self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18.]];
    CGContextRestoreGState(context);
}
CGContextRestoreGState(context);

Modal views in universal iOS apps

On the iPhone, it’s quite normal to have an app slide a sheet up to create something or change settings. But on the iPad, that same view should be displayed in a popover. In a universal app, the key is shared code and XIBs for both approaches.

The code below is all that I needed to have a Settings sheet appear correctly in both an iPhone and iPad app. I simply identify the device type and then either present a popover controller or a modal view controller, using the same XIB. Clean code. Consistent interface.

// Main View Controller
- (IBAction)showSheet:(id)sender {
	if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
		// iPad
		if (!self.popoverController.popoverVisible) {
			self.popoverController = [[[UIPopoverController alloc] initWithContentViewController:self.myNavigationController] autorelease];
			self.popoverController.popoverContentSize = CGSizeMake(400, 400);
			self.popoverController.contentViewController.contentSizeForViewInPopover = CGSizeMake(400, 400);
			self.popoverController.delegate = self;
			// Present the popover from the bar button.
			[self.popoverController presentPopoverFromBarButtonItem:self.navigationItem.rightBarButtonItem permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
		}
	}
	else {
		// iPhone
		[self.navigationController presentModalViewController:self.myNavigationController animated:YES];
	}
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)aPopoverController {
	[self.popoverController dismissPopoverAnimated:YES];
}

- (MyNavigationController *)myNavigationController {
	if (myNavigationController == nil) {
		self.myNavigationController = [[UINavigationController alloc] initWithRootViewController:self.mySheetController] autorelease];
	}
	return myNavigationController;
}

- (MySheetController *)mySheetController {
	if (mySheetController == nil) {
		self.mySheetController = [[MySheetController alloc] initWithNibName:@"MySheetView" bundle:nil] autorelease];
	}
	return mySheetController;
}

// Sheet View Controller
- (void)viewWillAppear:(BOOL)animated {
	self.contentSizeForViewInPopover = CGMakeSize(400, 400);
	[super viewWillAppear:animated];
}

Ad-Hoc and App Store IPAs with xcrun

Xcode 4′s “Build and Archive” feature is extremely convenient. With it, I can build a distribution version of an app and archive it away, so that later, in Xcode Organizer, I can click on that instance and submit it to the App Store. Brilliant for individual developers. But at my work, we wanted a bit more: automation and multiple IPAs from a single binary. Both are easy with xcrun, part of Apple’s Developer Tools. (Thanks to Stackoverflow for the pointer!)

Standardized Keys and Certificates

At work, we wanted a standard set of keys and certificates to use for official builds. I went through the standard Apple Developer Center process of creating a certificate signing request with Keychain Access to generate the local keypair, and I created a distribution certificate with that signing request and loaded that into Keychain Access. Then in Keychain Access, I exported the certificate as distribution.cer and the private key as distribution.p12. Xcode needs these two files to correctly use the provisioning profile.

One small hiccup was “login” versus “System” in Keychain Access. I easily imported those two exported files into the “login” keychain in Keychain Access. However, Keychain Access would not import the private key into the “System” keychain and gave the following error: “An error has occurred. Unable to import an item.” I needed to use the “System” keychain, as our build system runs with elevated privileges. To get around this error, I simply needed to import the certificate and private key via Terminal instead:

# Run in Terminal.
sudo security import /path/to/distribution.cer -k /Library/Keychains/System.keychain
sudo security import /path/to/distribution.p12 -k /Library/Keychains/System.keychain

Don’t forget to change the “Access Control” for the private key to “Allow any application to access this item” for “System.keychain”. Otherwise, you’ll get a build error about “user interaction is not allowed”.

At this point, I had a copy of the certificate and private key in “login” and “System” in Keychain Access. I simply needed two provisioning profiles based on that certificate: an ad-hoc profile and an app store profile. I generated both in the iOS Provisioning Profile, Adhoc.mobileprovision and Appstore.mobileprovision, and imported those into Xcode.

With the certificate, private key, and two provisioning profiles loaded up, automating the build was straight-forward. In fact, by selecting the correct default configuration (Xcode 3.2.6), I can simply run Xcodebuild with no arguments to generate an App Store distribution app bundle.

Multiple IPAs from a single binary

Automated builds are nothing new or novel. There are some great suggestions for how to structure your Xcode configurations to simplify automation. What I really wanted were two files at the end: App.AdHoc.ipa and App.AppStore.ipa, ready to be tested ad-hoc or submitted to the App Store. Enter xcrun.

Apple distributes xcrun as part of its Xcode Developer package. The command line utility will take an app bundle and convert it into an IPA file with the supplied provisioning profile. The key part is the supplied provisioning profile can be different from the original one. Here is how I call it:

xcrun -sdk iphoneos PackageApplication
   /path/to/bundle/AppName.app
   -o /path/to/bundle/AppName.ipa
   --sign "iPhone Distribution"
   --embed /path/to/certificate

With this command, I can run a build and then generate two IPA files: one for ad-hoc distribution or one for App Store submission. All with the same binary. The community and the tools have progressed a bit since furbo’s “The final test” in 2008.

Over-The-Air ad-hoc distribution

Apple added support for over-the-air (OTA) distribution in iOS 4. It’s the best way to distribute builds to QA, much better than syncing through iTunes. These instructions worked perfectly for me. You don’t need an Enterprise iOS account to do this; I think that simply lets you get around managing UDIDs. Seriously, it’s easy and awesome.


Deleting large AWS S3 buckets

2011.12.08 Update: Amazon now supports multi-object delete in a single API request, up to 1k objects.

Amazon’s AWS S3 is awesome. However, there are times when the API makes certain tasks a bit hard. Today, I wanted to delete a bucket with 100k files. AWS does not support deleting a bucket with files inside; you have to delete the files first.

First, I tried aws/s3. It supports :force when deleting buckets, in case they have files in them. That call was taking a long time, so I poked around the code. Unfortunately, the gem didn’t make some magic API call. It just looped:

def delete_all
  each do |object|
    object.delete
  end
  self
end

Next, I tried to find out how geemus’s brilliant fog gem handles this need. It doesn’t, because providers don’t.

Finally, I gave up that one API call dream and looked for what other people do. They use pagination and threads, and I ended up forking SFEley’s s3nuke script from GitHub. In the process of porting his solution from RightAWS to Fog, I discovered that fog handles auto-pagination internally. No need to mess with is_truncated or :marker. Quite nice.

This Ruby script deleted an S3 bucket with 100k files in 15 minutes.


iPhone Tip: Tether to your Mac

No, not the iOS Personal Hotspot. I meant the other direction. Here is how to share your Mac’s internet connection with your iPhone or iPad.

Simply go into System Preferences and click on “Sharing”. In “Sharing”, select “Internet Sharing” and click “Wi-Fi Options…” to configure the network name. Then, just check the box for “Internet Sharing”.

Very useful in some instances.