1. iOS DevCorner: Attaching an info panel to a UIScrollViewIndicator like in the Path 2 app

    Last week the new Path 2 app was released with a bunch of great UI tweaks, which I promise we will definitely see in other apps really soon.

    One of the most impressive one to me was the info panel attached to the scroll view indicator which appears as you begin scrolling and disappears after the scroll view stops.

    In contrast to the existing table view sections the info panel approach is non-persistent. It allows you to temporarily group data in a table view. Therefore it is not qualified to simply replace every table view section in your app, but it could be a good alternative or even an addition in some cases. So let’s dive into one possible solution I’ve figured out how to create an attached info panel.

    The Basics

    A UIScrollViewIndicator is the top most subview of a UIScrollView or of one of it’s subclasses, e.g. a UITableView. The indicator itself is simply an UIImageView. Unless you are not scrolling it’s alpha is 0 and therefore not visible - but it’s there right from the beginning. When the user scrolls to the top or bottom and drags it’s finger a little further – like a rubber band - the size of the indicator shrinks until it finally became a dot.

    The Solution

    The basic idea is to add the info panel’s root layer as a sublayer to the indicator’s root layer. With that, the info panel scrolls smoothly and stays in parallel to the indicator automatically. In the case were the scroll view hit the edges, we want the info panel to stay visible at the top or bottom of the screen, so we need to do some extra calculation to let it stay centered.

    The Code: Interface

    Here we just need four ivars which are quite self-explanatory:

    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <QuartzCore/QuartzCore.h>
    
    @interface FMInfoPanelViewController : UIViewController 
    {
    	UIScrollView *scrollView;
    	UIView *infoPanel;
    	CGSize initialSizeOfInfoPanel;
    	CGFloat initialHeightOfScrollIndicator;
    }
    @end

    The Code: Implementation

    In our implementation we first create an UIImageView which simply acts as a background of our info panel. After that we create the info panel itself with a negative x-origin so it will appear left to the scroll indicator.

    #pragma mark - 
    #pragma mark View lifecycle
    
    - (void)viewDidLoad
    {
    	CGRect viewFrame = [[self view] frame];
    
    	scrollView = [[UIScrollView alloc] initWithFrame:viewFrame];
    	[scrollView setDelegate:self];
    	[scrollView setContentSize:CGSizeMake(viewFrame.size.width, viewFrame.size.height * 4)];
    	[[self view] addSubview:scrollView];
    	
    	UIImageView *backgroundImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"infoPanel.png"]];
    	initialSizeOfInfoPanel = [backgroundImage frame].size;
    
    	infoPanel = [[UIView alloc] initWithFrame:CGRectMake(initialSizeOfInfoPanel.width * (-1), 0, initialSizeOfInfoPanel.width, initialSizeOfInfoPanel.height)];
    	[infoPanel addSubview:backgroundImage];
    	[backgroundImage release], backgroundImage = nil;
    }
    

    scrollViewWillBeginDragging: When the user starts scrolling we check whether the info panel is currently visible or not. If not we will add it’s root layer as as sublayer of the scroll indicators root layer. One important thing I’ve figured out is to set the background color of our info panels layer to the same as the scroll indicator’s root layer. Otherwise the scroll indicator wont be translucent anymore.

    #pragma mark -
    #pragma mark Scroll view delegate
    
    - (void)scrollViewWillBeginDragging:(UIScrollView *)aScrollView
    {
    	if (![infoPanel superview]) 
    	{
    		UIView *indicator = [[aScrollView subviews] lastObject];
    		CGRect indicatorFrame = [indicator frame];
    		initialHeightOfScrollIndicator = indicatorFrame.size.height;
    		
    		[[infoPanel layer] setBackgroundColor:[[indicator layer] backgroundColor]];
    		[[indicator layer] addSublayer:[infoPanel layer]];
    
    		// Center the info panel
    		CGRect infoPanelFrame = [infoPanel frame];
    		infoPanelFrame.size = initialSizeOfInfoPanel;
    		infoPanelFrame.origin.y = indicatorFrame.size.height / 2 - infoPanelFrame.size.height / 2;
    		[infoPanel setFrame:CGRectIntegral(infoPanelFrame)];
    	}
    }

    scrollViewDidScroll: As our info panel is already attached to the scroll indicator we can avoid a lot of unnecessary calculations when the scroll view just scrolls within it’s content area. Though we have to do some work when we reach the top or bottom of the screen:

    • When the scroll indicator starts shrinking we have to calculate the new center of our info panel.
    • When the scroll indicator’s height became less that the info panel’s height and we are on the top of the screen we don’t need to do any extra work as the info panel should stay at it’s current position.
    • When we are at the bottom of the screen we need to calculate a negative y-origin so the info panel will stay at the bottom while the scroll indicator continues to shrink.
    - (void)scrollViewDidScroll:(UIScrollView *)aScrollView
    {
    	UIView *indicator = [[aScrollView subviews] lastObject];
    	CGRect indicatorFrame = [indicator frame];
    
    	// We are somewhere at the edge (top or bottom)
    	if (indicatorFrame.size.height < initialHeightOfScrollIndicator)
    	{
    		CGRect infoPanelFrame = [infoPanel frame];
    
    		// The indicator starts shrinking, so we need to adjust our info panel's y-origin to stays centered
    		if (indicatorFrame.size.height > infoPanelFrame.size.height + 2) 
    		{
    			infoPanelFrame.origin.y = (indicatorFrame.size.height / 2) - (infoPanelFrame.size.height / 2);
    		}
    		// We are at the bottom of the screen and the indicator is now smaller than our info panel
    		else if (indicatorFrame.origin.y > 0)
    		{
    			infoPanelFrame.origin.y = (infoPanelFrame.size.height - indicatorFrame.size.height) * (-1);
    		}
    
    		[infoPanel setFrame:infoPanelFrame];
    	}
    }

    scrollViewDidEndScrollingAnimation: As we are good citizens we will remove our info panel when the scroll view stops scrolling.

    - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
    {
    	[infoPanel removeFromSuperview];
    }

    The Additions

    The Path 2 app has some nice additional animations to fade in and fade out the info panel. It also stays visible for a second after the scroll view has stopped scrolling so the user knows if he scrolled to the right position.

    The Files

    I’ve created a public github gist and Xcode sample project so you can download the source files.

    I hope this solution gives you an idea of how to implement this great UI approach to your app to increase the user experience. If you have any suggestions on how to solve it better, more easily or in a completely different way, please contact me @FlorianMielke.

    Take care,
    Florian

    December 6th, 2011