Caleb Davenport

Debugging Apple Frameworks

I really enjoy digging through the private implementation details of the frameworks that ship on iOS and macOS. There is a lot of fun stuff to mess with, sometimes just for fun, other times to work around a bug or figure out why something doesn’t work the way I expect.

Recently I had attempted to use DateComponentsFormatter.allowsFractionalUnits for a second time. In both cases I needed to display decimal hour values (“1.5h” instead of “1h 30m”) given a time interval. The documentation says this is a supported operation. Every combination of flags I try returns inconsistent whole number values instead of the decimal values I need. The first time I gave up and moved on without it. This time I thought it would be interesting to figure out exactly why it seems to be broken.

Runtime Headers

The first tool I reach for when I want to look into private framework details is iOS Runtime Headers. Runtime headers provide an easy way to browse all Objective-C types and their members (even the private ones). This is super handy if you want to learn how a system works at a high level or find some class or method to message without digging into the binary. (Like did you ever wonder how UILabel calculates its intrinsic content size with multiple lines of text? Boom.)

In this case I am looking for information about NSDateComponentsFormatter. There aren’t many interesting things in its header. It’s mostly a bunch of properties and a few methods that return strings. It does have an NSNumberFormatter called _unitFormatter which makes sense given this class is concerned with formatting individual pieces of information about time instead of whole dates.

I can’t get much more from the runtime headers so off to Hopper I go.

Hopper Disassembler

Hopper turns a compiled binary into something that can be navigated almost like the code I write every day. It looks really confusing at first so I’ll go through a few quick steps to get a binary loaded before I move on.

Hopper right after you import a binary.

There are a few more things to do to make Hopper nice to use. First, Hopper is really great at picking up Objective-C information. The sidebar on the left let me quickly search for the symbols I knew I wanted to look at. Second, the highlighted toolbar item in the screenshot below tells the main code view to show pseudo-code instead of assembly. And third, the toolbar items on the far right let me collapse those drawers I haven’t figured out how to use yet. Now I’m ready to go.

I started with stringFromTimeInterval: since that is the formatting method I call in my app. After looking at that and the other public formatting methods, I found that all of them eventually call a private _stringFromDateComponents: method. I might have seen that in the runtime headers if I had known what I was looking for.

Hopper showing the pseudo-code body of -[NSDateComponentsFormatter stringFromTimeInterval:].

The pseudo-code for _stringFromDateComponents: is really long. It handles all permutations of formatter settings and is solely responsible for assembling the various components into a final output string. Searching that method for anything related to allowsFractionalUnits showed no results. I was able to find that fractional units is used to apply maximumFractionDigits to the _unitFormatter we saw previously in _ensureUnitFormatterWithLocale: which is called every time a string is requested. So I know that the number formatter is told it can show digits but not why it doesn’t.

It wasn’t until after I looked closely at all stringFromNumber: calls that I found the problem. Every number passed to stringFromNumber: is created with +[NSNumber numberWithInteger:]. I was able to confirm in a separate test that even if a number formatter is configured to show decimals, and even if numberWithInteger: is called with a floating point value, the integer number will be a whole number when formatted. This makes perfect sense and matches the behavior I and others see in DateComponentsFormatter.

Hopper showing one of a handful of calls to -[NSNumberFormatter stringFromNumber:].

I initially suspected that this error was due to bad compiler inference around an Objective-C number literal. I tested that as well and found that the pseudo-code generated by Hopper for number literals looks different than the class function calls I see in _stringFromDateComponents:. I doubt that is the issue here.

Wrap it Up

I usually consult the Swift Foundation project too but there is no open source implementation of DateComponentsFormatter yet. I filed a bug and it was marked as a duplicate. Let me know if you find this post useful or have any questions! I’m @calebd on Twitter.