Sliding clocks around the screen a la Path

Nov 30, 2011 16:57 · 832 words · 4 minute read

The new Path app has hit the App Store, and the updated UI is GORGEOUS.   I was quite intrigued by the little touches, particularly the little clock icon that slides up and down the right-hand side of the screen as you scroll down the timeline. It looks really neat, and it got me wondering how it was done.

Turns out, it’s actually not that difficult (although it’s still a lovely UI touch even so).

The main view in Path is a heavily-customised UITableView sat inside a UINavigationController (although the UINavigationController bit is irrelevant at the moment). UITableViews are themselves heavily-customised subclasses of UIScrollView, which means that all the goodies that a UIScrollView provides are available to instances of UITableView.

There’s one particular goodie that’s relevant here. This is UIScrollView's contentOffset property, which shows how far from the origin the scrollView - or in this case, our tableView - has been scrolled. When the table’s at the top, the contentOffset's y value will be zero - but if you had a table that was (say) 1000 pixels high, that contentOffset would gradually increase until at the very bottom of the table it had a y value of 1000 pixels.

Another property that’s relevant here is the tableView’s contentSize property. If you access that after the table has finished loading - ie in the viewDidAppear method - then this will return the total height of the table with all rows fully-populated.

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

maxTableHeight = self.tableView.contentSize.height;
frameTableHeight = self.tableView.frame.size.height;
workingTableHeight = maxTableHeight - frameTableHeight - 64;

NSLog(@"TableView maxTableHeight = %f", maxTableHeight);
NSLog(@"TableView frameTableHeight = %f", frameTableHeight);
NSLog(@"TableView workingTableHeight = %f", workingTableHeight);

}

Dividing the contentOffset.y value by the contentSize.height will give you how far down the tableView things have scrolled as a percentage value. When the table first loads, the contentOffset.y value will be zero, therefore we’d be 0% down the table. At the bottom, we’d be 100% of the way - and obviously varying percentages along the way.

The third value that will help out here is the height of the table’s frame - in other words, how much of the overall screen the tableView occupies. If you’ve got a status bar and a navigation bar, that’s the full height of an iPhone screen - 480 pixels - less 20 pixels for the status bar and 44 pixels for the navigation bar - a total frame height of 416 pixels.

Now imagine that you created a UIView of some description, and “floated” it as the front-most view so that it appeared on the right-hand edge of the screen. (If it’s a subView of the main window, then the origin remains constant and doesn’t move with the table itself.) It’ll have an origin with an Y position and a Y position - x will be somewhere between 0 and 320, depending on how far right it needs to be placed. And the Y position will control how far up or down the screen it appears.

If we calculate the Y position by taking the height of the tableView's frame and multiplying it by the percentage that the table has been scrolled, the UIView will appear to move up and down the screen in proportion to how fat the tableView has scrolled. It’ll start at the top of the screen when the tableView is at the top, and slide down towards the bottom as the tableView is scrolled. It won’t leave the visible screen regardless of how long the tableView actually is, because we’re calculating the percentage of the tableView's frame rather than its content size.

Because UITableView inherits from UIScrollView, it means we can use the scrollViewDidScroll: method of UIScrollViewDelegate as the “trigger” for calculating how far things have scrolled, and adjusting the position of the UIView with the little clock icon.

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {

// Work out positions
currentTablePosition = scrollView.contentOffset.y;
currentTablePositionPercentage = (currentTablePosition / workingTableHeight);

// Work out position of "block"
float blockOffset = (frameTableHeight * currentTablePositionPercentage);

float newYpos = 64 + blockOffset;

NSLog(@"tableView's scroll position = %f", currentTablePosition);
NSLog(@"blockOffset position = %f", blockOffset);
NSLog(@"current table position %%age = %f", currentTablePositionPercentage);

NSLog(@"New xpos = %f", newYpos);

theBlockView = theAppDelegate.theBlockView;
theBlockView.frame = CGRectMake(theBlockView.frame.origin.x, newYpos, theBlockView.frame.size.width, theBlockView.frame.size.height);

}

This is all very crude, not least because the calculations don’t take into account the size of the UIView that you’re moving around. Nor do they account for the fact that you can scroll a tableView or scrollView beyond 0% and 100% as it bounces at the top and bottom extents.

Path also takes things one stage further by figuring out which row the little icon thing is hovering over, and changing the value of the clock to reflect when the contents of that row were updated. There’s also some very neat animation as the clock times change by spinning the hands. But the basic principles of figuring out where the icon slider should be still hold.

Update - Florian Mielke has produced a much better explanation (and a library on Github) here