Archiving an iPad only App

(I've submitted Combat Imp to Apple and while I wait for approval I'm doing some clean-up which includes verifying problems and writing up radars. There may be a handful of these posts coming up.)

Combat Imp is an iPad only project. Way back when I had just plunked some icons in manually (into the .plist I believe? It's been a while.) As I got ready to ship the damn thing I switched over to using an Asset Catalog for the icons and launch images. I only set up the iPad icons, because why include other sizes that wouldn't be used? And this was good.

At some point later in time I went to make an archive (inside Xcode - I was getting ready to make a beta build for testers.) The archive didn't have the nice icon, instead it had the generic grid/circles placeholder icon. Turns out that even in an iPad only app, the archive uses the iPhone iOS 7 icon and doesn't look for other icons if that slot is blank.

Not a huge deal but I was having some other icon problems at the time and I conflated two problems and went down a rabbit hole. So if your Xcode archive is lacking an icon, try adding an iPhone icon.

I wrote it up as radar 18252465 if somebody wants to dupe it. It happens on both Xcode 5.1.1 and Xcode 6 beta 7.

Road Trip 1.1 is Available

Did you get a shiny new iPhone 5? Well, an elongated version of Road Trip awaits you in the App Store! Support your favorite independent iOS developer by picking up a copy. If you've purchased it already then thank you and be sure to get the update.

If you do buy (or bought previously! Even if you rated 1.0 you can rate each version separately. Annoying but true.) a copy please take a moment to at least rate the app. I know, I know, I hate app developers begging for ratings as much (and probably more) than most people but it really does make a huge difference to sales. It amounts to the only marketing that can be done inside the App Store proper and Apple uses that data to drive how visible it is in search results and category lists. Version 1.0 never got enough ratings to display an aggregate and now that 1.1 is in the wild the review count is reset back to zero. I won't put a begging dialog in the app but I'm not above asking for ratings on the blog …

(If you're new around these parts you can read my story about developing Road Trip.)

App Store Link

UITableViews do not properly animate on an Airplay secondary screen

A few weeks back I ran into an interesting UITableView bug and I finally got around to writing up a test case and submitted a Radar to Apple. The test application source is on my GitHub and I've copied the radar to Open Radar.

I'm working on an app right now that has the option of driving a second screen. There are two methods of connecting such a screen: using a special dock cable to physically connect a monitor; and using Airplay to connect to an Apple TV device and using that screen.

Note that I'm not talking about mirroring the iPad display to the second display, I'm talking about having a completely different UIView on the second display. (This is confusing because iOS calls turning that on "Airplay Mirroring" although it's entirely up to the application whether it actually mirrors the main view or not.)

This second display is primarily a UITableView and it can potentially have quite a few rows to display. Also the intent is to have a screen that can be read at a distance so I use quite a large font (the current "large" size font is 36 point.) Since there's no touch UI there's no way for the user to directly scroll the secondary display but the app scrolls the second display to match a similar table on the iPad display. As the user manipulates the main display the secondary screen scrolls in unison. Well, at least it should and that right there is the bug. In my experience any UITableView method call that takes an animated: parameter does not animate on the Airplay secondary screen if animated: is set to YES. This works fine on the physical secondary screen, but not the Airplay. For example scrollToRowAtIndexPath:atScrollPosition:animated: will silently fail if you try to animate it.

There's also a workaround I have, and you can see it in the code on GitHub. Short form is this: animating the contentOffset value of the table in a GCD block via [UIView animateWithDuration:delay:options:animations:completion] works. So instead of simply scrolling to a row, I calculate a contentOffset for that row and scroll the underlying UIScrollView.

I was not able to find any references to this bug anywhere via Google so I'm not sure it's been reported before. If you want to animate a UITableView on an Airplay secondary screen you probably want to take a look at the GitHub repository.

MKMapView is a Naughty Minx

OK, that was nasty to find. I've been plagued on and off by a crash bug that I only seem to see on the iPad and it's never been very reproducible. Today I was putting textures in the background of the iPad screens and found a reproducible crash. Hooray! Except as soon as I put a breakpoint in the crash would go away so there's some sort of race condition/multithreading going on. Mumble grumble.

The breakpoint was everyone's friend EXC_BAD_ACCESS. Of course this means the bad code wasn't anywhere near the actual crash. (And as an aside, surprising to me because I use ARC and therefore I don't write the retain and release code. So there's probably an ARC bug in here as well. Later I'll try to make a test app and submit a Radar.)

How did I find it? That in and of itself is a convoluted story and maybe worth relaying. I turned on NSZombieEnabled (in Xcode 4 you go to the Edit Scheme window and there's a checkbox in the Run/Diagnostics panel for Enable Zombie Objects) and discovered it was my TripMapViewController class receiving a respondsToSelector: message after it had been deallocated (and after the view wasn't onscreen anymore). This is a bit of a red herring because I use respondsToSelector: a fair amount so I scrubbed through all of my code and verified that I wasn't the culprit.

OK, I have to admit that stumped me for a moment. So it would appear that some other system thread is calling respondsToSelector with a pointer that it snuck past ARC. I did something hacky just to confirm the situation: I stashed a copy of the TripMapViewController in a static pointer, which should mean ARC no longer released the object. That worked and the crash stopped. Now what? After that I realized my object was still around and that I could overload respondsToSelector: and at least see the damn callstack. Turns out this controller gets a lot of respondsToSelector: messages; enough that you can't just stick a breakpoint in there. I did some more clunky things so that I could turn on a breakpoint after viewWillDisappear had been called. Sure enough, this big pile o' hacks worked for me: I could see the callstack of a bogus call. Problem is that it was all system stuff, with several calls just labeled "MKLongHash". If you Google MKLongHash you only get one hit which turns out to be a Stack Overflow thread about ARC and messages to zombie MKMapViews. Oh really?

Sure enough that's it. If you set the delegate of a MKMapView to a controller it is possible that ARC will incorrectly release the controller while some MKMap thing is still messing about. The solution provided in the article is to explicitly set and clear the delegate on viewWillAppear/viewWillDisappear. This worked for me. Maybe the next person who Googles for MKLongHash will find this helpful :-)

Oh no you didn't VoiceOver

OK, let's say you're making an iOS app. And it's new so you go to use storyboards because they are awesome and you can target iOS 5.x. Swell. And you're going to use UITableViews and storyboards have that hot new prototype thing where you can lay out your cell right there in the UITableView and you're guaranteed you'll always get a cell from dequeueReusableCellWithIdentifier. Awesome! Well not so fast partner. Go read this Radar and this Radar. Yeah, that sucks. Short version is that works fantastically unless your user has turned on VoiceOver, at which point you don't get a cell at all. The stub code Xcode generates at that point will create a generic UITableViewCell with none of your custom layout. If you're lucky you'll get a blank cell. If you're unlucky your code assumed it got a custom cell type and crashes on some property access or method call. So what do you need to do? You have to old school it and just ignore the new layout feature. You can't require your users to turn off VoiceOver, that's not reasonable. You can leave the prototypes in and they will work in the VoiceOver off case but as far as I can tell there is no way to load a prototype cell from a storyboard. I suspect that you might just be able to rummage around in the nib and find a prototype but I don't see a proper way to do that. In the VoiceOver case you have to provide code that will load a cell from a nib file. You can leave the prototype cells in and that will work in the non-VoiceOver case but now you've got two cell prototypes and that's a bad idea. I do recommend leaving the call to dequeueReusableCellWithIdentifier in place though. Even with VoiceOver on the table can recycle old cells so if you do all of the cell identifier stuff then after a while your view can get a working set of cells that it just reuses on a scroll. Here's a code snippet that rolls this all up. It's not revolutionary but it gets the job done.
TripTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// That call is supposed to always work, but it doesn't if the user has VoiceOver on. If they do, we'll engage le hack below
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TripTableViewCell" owner:self options:nil];
// The above will load our blankCell field with a new TripTableViewCell.
cell = blankCell;

With this code you make a nib that contains your custom cell. Then in the view controller you make an IBOutlet like so:
@property (nonatomic, retain) IBOutlet TripTableViewCell* blankCell;
In the nib you set the File's Owner to the controller and connect blankCell to the cell. Like I said, nothing shocking here just some old-school-ness in the new storyboard world …