Animating Multiple Virtual Pages Using a NIB-based View

Today’s post is a simplified version of some work I’m doing for a client.

For the client app, I need to browse a set of data using a NIB-based view, but do so by swiping left and right in the context of a parent view — a similar effect as Apple’s weather utility app. I didn’t explore how Apple actually does it, but the method I am using is pretty straightforward.

Overview

Demo2010Dec19_pic1.pngThis simple app displays a view for a single item at a time from an ordered list of simple domain objects. A UISwipeGestureRecognizer detects swipe events which are used to animate items on and off the parent view.

The previous view is off screen to the left, and the next view is off screen to the right. Both of these are preloaded so that when a swipe event is recognized, there is no delay while loading data for the item to be animated onto the parent view. The app cycles through three instances of the same NIB-based view controller as the user swipes.

For instance, given a list of 7 items, the app starts in this state:

  • The center view displays item A;
  • The view off screen to the right is preloaded with item B;
  • The view off screen to the left is preloaded with the last item, item G;

In this state, when a left swipe event is recognized, I want it to be interpreted as “move the currently displayed item (A) to the left and show me the next item (B)”. What happens is this:

  • The position of the center view is animated to the left off screen position;
  • The position of the right view is animated to the center on screen position;
  • The old center view becomes the new left view;
  • The old right view becomes the new center view;
  • Since the old left view is “pushed” farther to the left, it becomes the new right view;
  • The new right view is loaded with the appropriate data from the list.

A mirrored set of events happens on a right swipe event. (The app treats the list circularly, so there is no right or left edge of items.) The app also includes a tap recognizer which is used to update the tap count for each item to demonstrate how you can maintain state as views are cycled.

Handling Gestures

Each instance of SampleViewController has three gesture recognizers, swipe left, swipe right, and tap. Each also retains a reference to it’s currently displayed item when it is loaded.

The behavior for tapping is simple. When a tap is recognized on a view, the tap count for that view’s item is updated. Nothing needs to happen outside the current view and item.

The behavior for swiping is slightly more involved. Since a swipe is supposed to initiate animating the current view off the screen and a new view into the center, it would be messy for the current view to handle the animation. The best solution is to have the parent view handle the animation, since it already owns all the items and views necessary.

The cleanest way to accomplish this is by creating a simple protocol for delegating the behavior. SampleViewController.h includes the protocol definition, and adds an ivar for the delegate itself.

SampleViewController.h

#import <UIKit/UIKit.h>
#import "SampleData.h"

@class SampleViewController;

@protocol SampleViewControllerDelegate
- (void) handleSwipeLeftFrom:(SampleViewController *) source;
- (void) handleSwipeRightFrom:(SampleViewController *) source;
@end

@interface SampleViewController : UIViewController {
    UILabel *sampleIdLabel;
    UILabel *nameLabel;
    UILabel *tapCountLabel;
    UIImageView *imageView;

    SampleData *sampleData;

    id<SampleViewControllerDelegate> delegate;

    UISwipeGestureRecognizer *swipeLeftRecognizer;
    UISwipeGestureRecognizer *swipeRightRecognizer;
    UITapGestureRecognizer *tapRecognizer;
}

@property (nonatomic, retain) IBOutlet UILabel *sampleIdLabel;
@property (nonatomic, retain) IBOutlet UILabel *nameLabel;
@property (nonatomic, retain) IBOutlet UILabel *tapCountLabel;
@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) SampleData *sampleData;
@property (nonatomic, retain) id<SampleViewControllerDelegate> delegate;

- (void) loadSampleData:(SampleData *) aSampleData;

@end

and in the implementation simply forwards to the appropriate method of the delegate:

SampleViewController.m snippet

- (void)handleSwipeLeftFrom:(UISwipeGestureRecognizer *)recognizer {
    [delegate handleSwipeLeftFrom:self];
}

- (void)handleSwipeRightFrom:(UISwipeGestureRecognizer *)recognizer {
    [delegate handleSwipeRightFrom:self];
}

- (void)handleTap:(UITapGestureRecognizer *)recognizer {
    self.sampleData.tapCount += 1;
    [self updateTapCountLabel];
}

The delegate owns the three instances of SampleViewController, so is able to properly manage the animation between views:

Demo2010Dec19ViewController snippet (SampleViewControllerDelegate implementation)

- (void) handleSwipeLeftFrom:(SampleViewController *) source {
    if (source == centerSVC) {
        [UIView animateWithDuration:SLIDE_DURATION
                         animations:^{
                             centerSVC.view.frame = leftFrame;
                             rightSVC.view.frame = centerFrame;
                         }];

        // move untouched view to other side, and adjust names for next cycle
        leftSVC.view.frame = rightFrame;
        SampleViewController *tempSVC = centerSVC;
        centerSVC = rightSVC;
        rightSVC = leftSVC;
        leftSVC = tempSVC;

        // cache next sample
        currentIdx = [self checkIdx:currentIdx+1];
        int rightIdx = [self checkIdx:currentIdx+1];
        [rightSVC loadSampleData:[samples objectAtIndex:rightIdx]];
    }
}

More to be done

There is much that can easily be done to enhance this example, some of which I will be adding to my client’s app. Things like:

  • Adding a UIPageControl at bottom
  • Adding fade-in/fade-out toolbars for extra navigation (like the Kindle App)
  • Implementing momentum on swiping so fast swiping moves past multiple views

Hopefully this helps someone get over the hurdle of animating between sibling views. I know I made several simplifications to my original code while preparing the sample code for this blog.

You may download the sample project and use the code however you wish.

Merry Christmas!!

I wish all of you a very Merry Christmas.

Make sure you take time to relax with family and friends this week, but also take some time to read and consider the original Christmas story.


We all need the support of others to do our best. Find other like-minded developers that will provide encouragement and motivation through local user groups, regular conferences or meetups. This post is part of iDevBlogADay which has really helped me stay on track with my writing and my iOS projects.

Also, here is a little more information about me, Doug Sjoquist, and how I came to my current place in life. You should follow me on twitter and subscribe to my blog. Have a great day!