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.
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
_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 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.
- Open File > Read Executable to Disassemble…
- Navigate to the framework you want to open. For iOS frameworks it’s best to open the simulator version. I want to view iOS
Foundationso I opened
- Hopper is going to ask a bunch of questions. I just hit next without changing any of the default options. ¯\_(ツ)_/¯
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.
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
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.