Adam Ernst

A replacement for NSURLConnectionDownloadDelegate

In my previous post I noted that NSURLConnectionDownloadDelegate would provide a slick way to download files to the filesystem in iOS 5—but it doesn’t work unless you’re using it in a Newsstand app.

If you’re an old hand at Cocoa, you may remember the NSURLDownload class. It did essentially the same thing: filesystem downloads from a URL. But it’s never been available on iOS.

Enter AEURLDownload. It’s a dead simple class to asynchronously download a URL to the local filesystem. It uses NSURLConnection behind the scenes and saves to a temporary file which is properly created with mkstemp, so it’s safe even for use in non-sandboxed apps. When the file finishes downloading, your completion block is called so you can move the file where you want it. If you’re looking for NSURLDownload for iOS, AEURLDownload is what you want.

It doesn’t have progress callbacks yet, but stay tuned; they’re coming soon.

NSURLConnectionDownloadDelegate doesn’t work

Surprise undocumented fact: NSURLConnectionDownloadDelegate is only for Newsstand apps. If you attempt to use it in a non-Newsstand app, it will appear to succeed but pass you a nonexistent file path.

More generally, Newsstand is pretty neat but it’s silly for Apple to restrict its functionality to news apps. For example, take my subway app: it could definitely benefit from downloading service change data daily if you’re plugged in at 4AM (which is what Newsstand allows you to do). But since I’m not a “news” app, I can’t do that.

AEURLConnection

I just published AEURLConnection, a simple library for iOS that lets you use iOS 5’s slick new sendAsynchronousRequest:queue:completionHandler: API on iOS 4.

It also helps you solve the nasty Deallocation Problem by guaranteeing that your completion block will be released on the main thread. Check it out!

On the Facebook iOS SDK

While I try to refrain from criticizing code in public, the Facebook iOS SDK is just too frustrating to ignore.

Let’s take just one method, used to display a Facebook style dialog:

        dialog:(NSString *)action
     andParams:(NSMutableDictionary *)params
   andDelegate:(id <FBDialogDelegate>)delegate 

Wait… I pass a mutable dictionary into this method?

Why is every keyword prefixed with and, a practice that is specifically forbidden by Coding Guidelines for Cocoa?

Taking a step back, why am I required to manage my own instance of a Facebook object? Only one user should be logged in at a time, so why can’t Facebook offer a singleton instance in +[Facebook sharedInstance]?

Most annoying is Facebook’s makeshift attempt at implementing SSO (single sign on) via the Facebook SDK. If you use the Facebook SDK in your app users are bounced out to Safari to log in to Facebook, in the hope of capturing existing Facebook login cookies. Edit: It may be that sending the user to Safari is a security measure, too. In practice this behavior is disruptive and undesirable, but there’s no way to turn it off without forking the SDK.

Silliest of all is that Facebook went to all this trouble for SSO, and then apparently blew off Apple when they offered to build it into iOS 5.

Be careful with performSelector

The following can fail in a very mysterious way:

- (void)locationManager:(CLLocationManager *)manager 
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation {

    [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                   selector:@selector(timeout) 
                                     object:nil];
    [self setLocation:newLocation];
    [self performSelector:@selector(timeout)
               withObject:nil
               afterDelay:60*10];

}

It crashes on setLocation: with EXEC_BAD_ACCESS. It turns out self is dealloced during the method call! How?

Imagine an object with this method is released by all other objects holding references to it. There’s still one last object holding a reference though—the object itself, via performSelector:withObject:afterDelay:, assuming at least one location fix came in and the delay hasn’t passed.

A new location fix comes in, so the method needs to cancel the delayed method invocation. NSObject cancelPreviousPerformRequestsWithTarget: is just the thing, but that also releases self. Since that was the last reference, self is dealloc’d right out from under us! (The subsequent method call leads to EXEC_BAD_ACCESS.)

Be very careful calling cancelPreviousPerformRequestsWithTarget:self. An easy if inelegant fix is to insert [[self retain] autorelease] at the top of the method.

I wonder if ARC handles this situation properly?

Seeking an HTML5 Video Player on the iPad

The HTML5 spec for the <video> tag describes how to seek to a new position: set the currentTime attribute to a value that is within the range of the seekable attribute.

What is less clear is what events can trigger a change in the seekable attribute. At what point in the loading process are you allowed to seek to a new position?

The answer is not straightforward. Here is the approximate sequence of events when a video tag is initialized (omitting the frequent “progress” event):

loadstart
durationchange
loadedmetadata
loadeddata
canplay
canplaythrough

Based on my tests, on the desktop (Safari 5.0) seekable is set to the range of the entire video as durationchange fires. As soon as the <video> tag knows the duration (which happens very early on), you’re allowed to seek anywhere in the video.

It’s a different story on iPad (MobileSafari iOS 4.3). The same event sequence occurs but seekable is usually not updated until the canplaythrough event. What’s going on?

Based on lots of logging code, It turns out that seekable is usually being filled in sometime between canplay and canplaythrough, but we have no way to detect the change since there is not a specific event for changes in seekability. No event—not even the frequent progress event—fires when the seekable ranges change.

The takeaway: don’t assume you can seek at any time, or even that you can seek after the duration of the video is known. You may want to listen for all video-related events, manually inspect the seekable attribute after each event, and only set currentTime if it’s included in the seekable time ranges. Or, if you absolutely must seek as soon as you can, set a repeating timeout to continually check seekable.

UITableViews in UISplitViews

iPad’s Mail uses a UITableView hierarchy as the master view in a UISplitView. There are a number of subtle behaviors worth noting if you want to copy this style.

  1. Scroll position is saved for each mailbox; if you return to the same mailbox, you’ll be at the same scroll offset. Unless a row was selected but scrolled out of view when you left; then the table will be scrolled just enough to bring the selected row back into view.

    Here’s where Apple really shines: if the selected row was scrolled off the top, it appears at the very top of the UITableView. If it was scrolled off the bottom, it appears at the very bottom.

    scrollToNearestSelectedRowAtScrollPosition:animated: to the rescue. Specify UITableViewScrollPositionNone. This gives us exactly the right behavior:

    The table view scrolls the row of interest to be fully visible with a minimum of movement. If the row is already fully visible, no scrolling occurs.

  2. In portrait orientation when you select a new message, the table popover doesn’t immediately disappear. There’s a short delay (about 0.3 seconds feels right) before the popover is dismissed.

    You could use performSelector:withObject:afterDelay:, but with Obj-C blocks there’s an even cleaner way:

    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), 
                   dispatch_get_main_queue(), 
                   ^(void){
                       // React to new selection accordingly
                       [[self popoverController] dismissPopoverAnimated:YES]
                       [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                   });
    
  3. If you’re in landscape orientation, entering a new mailbox automatically selects the first message in that mailbox.

    In portrait orientation, if you navigate into a different mailbox the first message is not automatically selected. What’s more, if you dismiss the popover without selecting a message in the new mailbox and then re-open the popover, be ready for deja vu—you’re right back in the original mailbox. Your navigation state is lost unless you select a message. Interesting!

    This last behavior is not trivial to implement, but it’s what makes Mail such a joy to use. You never get lost.