Dissecting UIDatePicker
The view hierarchy is denoted by indentation so you may want to find the view you are focusing on and call -layoutSubviews on it again. You can send messages to raw memory addresses in lldb.
Some insights from this:
- UIDatePicker uses 3 table views (UIPickerTableView) per section. That means 9 table views for a date picker and 12 for date and time. These three table views are portioned vertically, one for the grey cells above the selection area (red), one for the selection area (purple), and one below (green).I like to set the background color on various views with lldb to get a sense of what they do.
(lldb) e [(UIView*)0xa22fe00 setBackgroundColor:[UIColor colorWithRed:0 green:1.0 blue:0 alpha:0.5]];
- Each of these table views uses 10,000 rows. (We found a table view at 0xe94ac00 earlier)
(lldb) p (NSInteger)[0xe94ac00 numberOfRowsInSection:0](NSInteger) $8 = 10000
- The cylindrical visual affect is caused by an affine transform on each cell’s layer in the top and bottom tableviews (one of these is pink in the image).
(lldb) p (CGAffineTransform)[[0x8cc8230 layer] transform(CGAffineTransform) $13 = (a = 1, b = 0, c = 0, d = 0, tx = 0, ty = 0.945817232)
What happens if you scroll to the end of the 10,000 cells? I assume they start you out near row 5,000, except for the year segment where you start at row 2014. When you scroll 5,000 cells up or down and get close to 0 or 9,999, it kindly scrolls you back to the midpoint. We implemented similar behavior in WFDatePicker.
Memory Implications of Infinite Rows
Although table view cells are created lazily, and reused when possible, UITableView allocates some space for each row. In Instruments, filter by UISectionRowData in Allocations List to see these.
UIDatePicker allocates one block of 78.5 KB and another of 39.5 KB per table view (of 10,000 rows). With 9 of these, that’s about 1 MB per UIDatePicker. Not too much for an iPhone to handle, but you should think twice before using more than 10,000. We found it is around 8 + 4 bytes per row. 100 million cells causes a noticeable lag and 1 billion cells caused my test app to become unresponsive.
Our implementation uses three table views of 10,000 cells each, coming in around 350 KB. We didn’t need 9 table views because we don’t need any transformations for visual effects.
Implementation of Infinite Cells
Sticky Scrolling
The zero case is fairly simple. We find the amount of “error” and scroll to the first or second top most visible cell, depending on the amount of error.
Testing
This date picker makes heavy use of UITableView. Since we don’t want or need to test UITableView itself, we can mock instances of it with OCMock and verify we are calling its API properly. This usually goes both ways, where we mock out UITableView methods that give WFDatePicker data, or expect methods that WFDatePicker needs to call to affect UITableView. The tests are usually near 1 to 1 with the WFDatePicker’s methods.
Here is the test for the method shown above, scrollToMiddleCell, which is called whenever a scroll view “lands” misaligned with a cell border.
A quick introduction to our testing methodology, which is discussed more here: we use OCMock and XCTest to verify accuracy of behavior and results in our components. The three cases I decided to cover here are when we land on a content offset such that we need to scroll up, when we need to scroll down, and when we don’t need to scroll. If a cell is 44pt, the first case would be something like 44 * 12 + 34, and the second might be 44 * 13 + 11. The last case could be 44 * 7.
Scrolling up or down is done by scrolling to the second or first visible cell (with UITableViewScrollPositionTop), respectively. In the third case we don’t want to do much except tell the component that a date is selected. In the first two cases this is done when the scroll animation finishes (via a UIScrollViewDelegate call).
Many of our tests follow this expectation-execute-verify paradigm. OCMock lets us verify the methods called on the UITableView object, and the accuracy of the parameters (line 13 and 22). We can also provide data that the UITableView object should, so we can test under specific conditions (line 11, 20, 30). On line 28 and 30 we create a partial mock of the object we are testing, so we can verify some internal behavior (userHasSelectedDateWithScrollView) in this case. Alternatively, we could have tested the results of the internal method (probably just a delegate call), but we prefer to keep the scope of each test very narrow in the event userHasSelectedDateWithScrollView changes. For this test, we just want to know it was called with the proper parameters.
Building UI components involves a fair dose of design, investigation, implementing, and testing. All of these things are important to iOS development here at Wealthfront.