A NSURLSessionTask Debug Story

While working on AsyncDisplayKit, we ran into a strange iOS compatibility bug. Our Kittens example project was working just fine for iOS 8, but when we ran the project on iOS 7 it instacrashed.

There was an unrecognized selector being sent to a NSURLSessionTask object. We were using the Objective-C runtime headers to slap on a metadata object for some convenience properties:

@implementation NSURLSessionTask (ASBasicImageDownloader)
static const char *kMetadataKey = NSStringFromClass(ASBasicImageDownloaderMetadata.class).UTF8String;
- (void)setAsyncdisplaykit_metadata:(ASBasicImageDownloaderMetadata *)asyncdisplaykit_metadata
{
  objc_setAssociatedObject(self, kMetadataKey, asyncdisplaykit_metadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (ASBasicImageDownloader *)asyncdisplaykit_metadata
{
  return objc_getAssociatedObject(self, kMetadataKey);
}
@end

Nothing special.

In order to debug the crash, I placed a breakpoint just before the setter that was crashing. I fired up the app on my iPhone 4 running iOS 7.1 and waited for the breakpoint to hit.

As soon as the Debugger window popped up, I used our chisel lldb extensions to inspect the class hierarchy of the object with pclass:

__NSCFLocalDownloadTask
   | __NSCFLocalSessionTask
   |    | __NSCFURLSessionTask
   |    |    | NSObject

That's odd. We are creating a NSURLSessionTask object. That class doesn't even show up in the dump.

Just to be safe, I printed the class hierarchy of the same object when running on iOS 8.1:

__NSCFLocalDownloadTask
   | __NSCFLocalSessionTask
   |    | NSURLSessionTask
   |    |    | NSObject

It appears that the NSURLSessionTask factory method downloadTaskWithURL: has some black-box magic happening on iOS 7.

Since __NSCFLocalDownloadTask doesn't inherit from NSURLSessionTask on iOS 7, that means that our associated object wont exist on our task object.

All we had to do was change the class to NSURLSessionDownloadTask and everything worked just fine. My hunch is that NSURLSessionTask is some sort of class-cluster similar to UIButton.

WatchKit and Threading

While working on some of the chapters for the upcomming WatchKit by Tutorials book, I assumed since WatchKit extensions bascially run as a Remote Procedure Call (RPC), writing asynchronous code wouldn't work. This was a bummer because running dynamic applications on Apple Watch would be very slow.

But I hadn't actually tried it.

At Facebook we have a saying that "Code Wins Arguments", so I decided to prove that something horribly, horribly wrong would happen when you wrote async code in WatchKit.

I wrote a really simple app to test a couple of things:

  • Threading in WatchKit
  • Updating the UI after willActivate: finishes
  • Updating layout after setting contents (like setImage:)

Experiment Storyboard Setup

Inside initWithContext: I download an image and set the contents of the WKInterfaceImage. I also set the contents of the label above the image with text that will wrap, just to push the watch layout system.

[self.label setText:@"Loaded at initWithContext:"];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  NSURL *url = [NSURL URLWithString:@"http://placehold.it/120x100"];
  NSData *data = [NSData dataWithContentsOfURL:url];
  UIImage *placeholder = [UIImage imageWithData:data];

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.image setImage:placeholder];
  });
});

To my surprise, the image downloaded, set the WKInterfaceImage contents, resized based on the image width and height, and pushed the label below the image down!

This worked?

Granted, this is on a simulator so we will have to see what happens when running on a real device and how the Bluetooth connection handles these async callbacks, but this is a great sign!

If you want to play with this project you can check it out on Github.

Sidenote: Yes I'm using Objective-C. Swift introspection sucks and doesn't work at all with Facebook's Chisel LLDB extensions.

The new UISplitViewController and iOS 7

Splitting all the things

tl;dr download the sample project

iOS 8 updated the UISplitViewController API to work on iPhones. It let's you specify when and how the split layout happens.

You end up with a universal storyboard which is very DRY.

However, I assumed that the new UISplitViewController only worked for iPhones running at least iOS 8.

Problem is that the iOS 8 adoption rate is really bad compared to previous versions. As of November 17, 2014 iOS 8 still has less than 60% of the market.

I thought that in order to use UISplitViewController I would have to scatter my code with OS if-statements or use multiple storyboards.

But it turns out that the new UISplitViewController APIs are fully compatible with iOS 7! Why Apple doesn't document this out is beyond me.

Yup, you can use a single storyboard with a UISplitViewController and have all of this without any compatibility checking:

  • UINavigationController on iPhone 4 with iOS 7
  • UISplitViewController with landscape split on iPhone 6+ with iOS 8
  • UISplitViewController always splitting on all iPads

There are probably even more customizations, these are just the ones that I've been playing with.

Getting it to work

This NSHipster article got me rolling with UISplitViewController. Using their pattern, you need:

  • UISplitViewController as the initial controller
  • Navigation controller as master with some controller as root (usually a table)
  • Navigation controller as detail with some controller as root
  • Something as the UISplitViewControllerDelegate

Use a Show (Detail) segue from your table cell to the detail navigation controller.

Interface Builder

In -prepareForSegue: of the master controller, you will need to check if the segue destination is either a UINavigationController (from a split view detail swap) or your detail controller (from a nav push).

var dest: DetailController
if let nav = segue.destinationViewController as? UINavigationController {
  dest = nav.topViewController as DetailController
} else {
  dest = segue.destinationViewController as DetailController
}

Finally, whatever is the delegate for your UISplitViewController needs to implement this long-ass delegate method:

func splitViewController(
  splitViewController: UISplitViewController,
  collapseSecondaryViewController secondaryViewController: UIViewController!,
  ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool
{
  return true
}

Check out the sample project on Github and try it for yourself. This project has been tested on iOS 7 on all simulators and an iPhone 4 as well as iOS 8 on an iPhone 6 and all simulators.