bdunagan
fill the void

Writing Retrospect for iOS

At WWDC 2009, Eric Hope talked about crafting the best iPhone apps at his iPhone User Interface Design session (the one where Cultured Code likened public speaking to kryptonite). The first step was coming up with an Application Definition Statement. It should define what your app is and what it isn’t. The statement should include a differentiator (your app’s strength), a solution (the problem you’re solving), and an audience (the people who care), or as my Ruby-esque notes read, “#{differentiator} #{solution} for #{audience}”. The goal of the statement was to focus your app. Mac apps could have a long list of features, but the iPhone was a far more constrained environment. Hope’s example was iPhoto. On the Mac, it views, edits, shares, prints, and what not; on the iPhone, it shares, nothing else.

That talk was almost three years ago. Things have moved forward. The iOS environment is less constrained, particularly with the iPad. Apps can do more, and iPhoto is a great example. In March, Apple released an iPhoto for iOS that contained editing abilities.

Retrospect for iOS has followed a similar path. What started as a very simple app has grown up a bit.

Retrospect for iOS 1.0

I wrote Retrospect for iOS 1.0 in December 2009, just for fun. For the Application Definition Statement, I came up with “a serious tool to monitor multiple Retrospect servers”. No control, just monitoring. I focused on activities. Viewing activities for a Retrospect server gives administrators a high-level view into what’s going on. If everything looks good, no action required; if not, they should launch Retrospect for Mac to dig deeper. Our designer hand-crafted a set of awesome icons, and we submitted it to the App Store.

The app was a moderate success. It was downloaded around 5,000 times and received a glowing review from Economist writer Glenn Fleishman (a long-time Retrospect user), who praised its “outstanding distillation”. The app didn’t do much beyond viewing activities, but it did that task well. Still, it didn’t replace Retrospect for Mac, and it didn’t support the iPad. Just as Apple updated its definition of what iPhoto for iOS was, I recently decided it was time for Retrospect for iOS 2.0.

Retrospect for iOS 2.0

What did the first version lack? Control. Complete monitoring. Better server support. iPad support. That’s the list I came up with in December 2011. I decided the app should display everything that the Mac version did: Activities, Past Backups, Scripts, Sources, Media Sets, Devices, and Reports. I even added badges to each report. I added full support for the latest Mac and Windows Retrospect servers and partial support for a couple versions before those. And I finally enabled control within the app: running scripts. I submitted v2.0 in February.

Close but not quite. While most of the new features were significant improvements, one failure erased all of those gains: syncing. Retrospect stores quite a bit of information, and when I expanded the list of items displayed on the iOS app, it translated into syncing times that were 10x to 100x longer. The UI prevented the user from looking at a server until it was synced, so the user might launch the app but then wait a long time before being able to monitor the server. Moreover, the iPad UI did not take advantage of the space. It looked like an iPhone app at “2x” but without the pixelation.

Retrospect for iOS 2.1

With v2.1, I fixed those two issues. Syncing is now done by item type. Users can view activities before media sets finish syncing. I also added split-screen support to the iPad UI to make better use of the space available.

This latest version of Retrospect for iOS is now available on the App Store. If you use Retrospect and have an iOS device, give it a try. It’s free.


rescue_from RoutingError in Rails 3

Rails 3.0 handles routing errors (ActionController::RoutingError) differently than Rails 2.3. The middleware addition means the error is passed to ActionDispatch, instead of ApplicationController. A bit ago, I actually removed the catch-all route at the bottom of routes.rb because I thought the ApplicationController::rescue_from handled any routing errors. Not true. Rails 3.2 doesn’t resolved it either.

There is an open GitHub issue for it: “Can no longer rescue_from ActionController::RoutingError”. (In fact, it’s old enough to be an issue migrated from Lighthouse.) There are several solutions posted inline in the issue’s comments. José Valim from the Rails core team suggests simply adding the catch-all route back to routes.rb; others suggest overriding ActionDispatch::ShowExceptions::render_exception. All seem focused on rendering the 404 page manually. However, I wanted to make rescue_from work. My solution is the catch-all route and raising the exception manually.

# routes.rb

# Any other routes are handled here (as ActionDispatch prevents RoutingError from hitting ApplicationController::rescue_action).
match "*path", :to => "application#routing_error"
# application_controller.rb

rescue_from ActionController::RoutingError, :with => :render_not_found

def routing_error
  raise ActionController::RoutingError.new(params[:path])
end

def render_not_found
  render :template => "misc/404"
end

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.