MPFlipViewController: a page-flipping container controller


I put a project called MPFlipViewController up on GitHub. It’s a page-flipping container view controller that allows the user to flip through a series of view controllers as if they were pages in a book. It is based on the MPFlipTransition class I already have on GitHub, but instead of being just a transition between 2 views, it is a full-fledged container view controller that supports panning and swiping between pages (child view controllers). It follows the Containment API introduced in iOS 5, so it behaves as a proper container view controller similar to the system container controllers (e.g. UINavigationController, UITabBarController and UIPageViewController).

Requirements

Xcode 4.3
iOS 5
ARC

API

The API is based on the API for UIPageViewController (since they fulfill an almost identical role). There is a data source protocol that you implement to specify the previous and next pages, and a delegate protocol that you implement in order to receive feedback on whether or not a page-turn operation completed and also to optionally specify the new orientation in the event that device orientation changes (i.e. user rotates the device).

Use

To create a flip controller use the initWithOrientation: method and pass in the desired orientation (horizontal or vertical):

- (id)initWithOrientation:(MPFlipViewControllerOrientation)orientation;


To set the content use the setViewController:direction:animated:completion: method where direction indicates whether the animation should be a page flip forward or backward.

- (void)setViewController:(UIViewController *)viewController 
                direction:(MPFlipViewControllerDirection)direction 
                 animated:(BOOL)animated 
               completion:(void (^)(BOOL finished))completion;

To enable touch gestures (panning and swiping between pages) implement the MPFlipViewControllerDataSource delegate to provide the previous and next pages (if any). Return nil for either method to indicate the user is already on the first or last page.

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
      viewControllerBeforeViewController:
          (UIViewController *)viewController;

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
       viewControllerAfterViewController:
          (UIViewController *)viewController;

To be notified of whether a page turn animation completed or not, set the MPFlipViewControllerDelegate and implement the optional flipViewController:didFinishAnimating:previousViewController:transitionCompleted: method. This method is only called if the page turn was gesture-driven (i.e. in response to a pan or swipe), and not programmatic (i.e. in response to a call to setViewController:direction:animated:completion:).

- (void)flipViewController:(MPFlipViewController *)flipViewController 
        didFinishAnimating:(BOOL)finished 
    previousViewController:(UIViewController *)previousViewController 
       transitionCompleted:(BOOL)completed;

To change the orientation of the flip controller when device orientation changes, set the MPFlipViewControllerDelegate and implement the optional flipViewController:orientationForInterfaceOrientation: method and return the desired orientation.

- (MPFlipViewControllerOrientation)flipViewController:
                       (MPFlipViewController *)flipViewController 
                   orientationForInterfaceOrientation:
                       (UIInterfaceOrientation)orientation;

Demo Project

The GitHub project includes a sample project that demonstrates the use of the control and its API.

Anatomy of a page-flip animation


In this post I would like to do a close examination of my take on the page-flip animation.  As with my foray into folding animations, this started with my desire to come up with some cool examples of real world use for matrix transformations for my Enter The Matrix presentation for CocoaConf Chicago last March (and which I’ll be delivering a new and improved version of at CocoaConf DC next month).

The Basics

Essentially we have two views that we are transitioning between.  Each one is divided in two along either a vertical or horizontal axis and rendered as an image.  If we’re transitioning from A-B to C-D and flipping from right to left, then we will start by flipping up B (while revealing D beneath it).  At the halfway point B will disappear (and D will be fully revealed), and we will begin flipping down C and covering up D.  By the end A & B will be hidden and only C & D will remain.



For my own purposes I refer to the half of the old view that we flip up (B) as the “front page”.  The other half of the old view that remains in place (A) I call the “facing page”.  The half of the new view that we flip down (C) is the “back page” (i.e. it’s on the back side of the page we’re flipping), and the half of the new view that stays in place (D) is the “reveal page”.  Whether one of these halves is on the left or the right (or the top or the bottom) depends of course on the direction of the flip (and its orientation), so I just like to think in terms of which ones are moving and which are not.

As described above, this is a 2-stage animation.  First we flip the front page up until it’s vertical and disappears.  Then we flip the back page down from vertical to flat.

Setting the Stage

Before we execute either stage, we need to prepare our layer hierarchy for the animation.  We will create a view with 4 sublayers where each sublayer’s content is an image rendered from a half each of the new and old views.  The layers for the facing and reveal pages go beneath the layers for the front and back pages (although we won’t add the back page to the hierarchy just yet).


The 2 lower sublayers (facing and reveal) will not need to move.  At this point we can add our animation view and hide (or remove) the old view as we’ve replaced it with a (static) copy.

Stage 1

In a right-to-left page flip, the front page is on the right.  We anchor it on its left side (the “spine” of our imagined book) and rotate it about the y axis from 0° to -90°.

Stage 2

The back page in a right-to-left flip sits on the left and so we need to anchor it on its right side (again forming the spine in the middle of our view).  At the completion of the stage 1 animation we can remove the front page sublayer from the hierarchy and add in the back page sublayer, already pre-rotated to +90°.  (Note: because the 2 layers are anchored on opposite sides, the sign of the angle to which they need to be rotated to appear in the same position is also opposite.)


Then we simply rotate the back page to 0°.

Making it better

What we have so far, serves as a passable page-flip animation, but we can do better in making it more realistic.  By adding a shadow layer (a simple CALayer with background color set to black and opacity set to maybe 0.5) to the reveal page and then animating its opacity, the page beneath can be initially dark (shadowed by the front page) and then get gradually lighter as the front page moves towards vertical.  So let’s animate the opacity of the shadow layer on the reveal page from 0.5 to 0 throughout the stage 1 animation.


During stage 2 we can add a matching shadow layer to the facing page and animate its opacity from 0 to 0.5 as the back page lowers down on top of it to hide it.

Next it would be nice to create the impression of a crease, of a differentiation between the flat facing page and the flipping front page.  This can be achieved using a shadow layer on the front page, although it doesn’t need to be as dark.  We can animate its opacity from 0 to 0.1 during the stage 1 animation.  We’ll add a similar shadow to the back page for stage 2 and animate it from 0.1 back to 0.

Thinking about the shadows

The width of the flipping page during the animation follows a cosine path (although this is slightly distorted by the viewpoint of our perspective, see below).  So for example at 50% completion of the first flip, the page is rotated to 45° but its width is cos(45°) × width, or 70.7% of the width.  If we animate the shadow on the reveal page linearly, that means the shadow would be only at 50% of its maximum even though the page is still 70% covered; i.e. the intensity of the shadow is not proportional to the width of the page covering it.  The reveal page will only be half revealed when the animation is 2/3 completed (cos(60°) = 0.5) but by then a linear shadow would be at 1/3 of its maximum.  So I decided that the shadows on the reveal and facing pages should animate along cosine and sine paths respectively to achieve a more pleasing effect (using CAKeyframeAnimation, see code).  This effectively means the shadows stay darker longer, so I also reduced their maximum opacity to 0.333.  I think it’s ok to leave the front and back page shadows as simple linear animations.

Antialiasing

Did you notice anything wrong with the screenshots above?  There are probably several things, but one of them is the jagged edge of the flipping page.


Because we’re using static images to perform our animation, it’s a simple matter to antialiase the edges in an efficient manner, which gives us this:


Thinking More About the Shadows

But a shadow on the front page doesn’t really make sense and darkening the entire page just to suggest a crease seems like overkill.  Instead we can use a CAGradientLayer to darken the page just near the crease, but leave the bulk of the page unmodified.  We’ll keep the animation on the shadow layer’s opacity to ease the effect in (alternatively, we could animate the gradient’s colors by increasing their alpha).

Timing Curves

Because this is a 2 stage animation, we need to pay some attention to the timing curves.  While we may want an Ease In Ease Out timing curve over the entire animation, that means using an Ease In curve over the first half and an Ease Out curve over the second half.  If we Ease In and Out over each half, we will end up with an uncomfortable pause in the middle of our page flip.  (Ok, it would probably only be uncomfortable to me having to watch such an animation but still.)

Perspective

For our animation to appear 3d at all, we will need to set the m34 component (skew) of the main layer’s transform.  If we don’t, our page flip will be completely flat and rather odd looking.

No perspective: m34 = 0

Skew is defined as – 1 / z where z is the height of the viewer in points above the flat plane of the view.  Choosing the right skew will be dependent upon the dimensions of the views you are animating.  Your skew should be proportional to your page width (horizontal flip) or height (vertical flip).  If z it too low, your page animation will be blown out of proportion.

Too much perspective: m34 = -1/225 or -1/(1.5 × page width)

If z is too high, your page animation won’t have enough “pop” to it.

Too little perspective: m34 = -1/1800 or -1/(12 × page width)

I find that anything in the range of 4-5 × width works pretty well for z, but you can experiment and adjust the animation to your taste.

Inverse perspective

If you invert your perspective (use a positive value instead of the typical negative one), you can get a pretty cool effect.  It doesn’t really make physical sense as a page-flip illusion, but is nonetheless interesting.  (Note:I’ve removed the shadow on the reveal page for this effect.)

Anchor Point

The anchor point of the layer being animated determines the view point of the perspective.  By default the anchor point is {0.5, 0.5} which corresponds to the midpoint of the layer.  That’s why the flipping page sticks out an equal distance above and below.  But if we change our anchor point, we could also change our perspective on our animation and thus alter its look and feel.

View from below – Anchor Point {0.5, 1.5}

View from above – Anchor Point {0.5, -0.5}

Room for Improvement

And now we have a fairly decent page-flip animation.  We could probably improve it further by adding realistic shadows for the front and back pages to cast upon the reveal and facing pages respectively (instead of a uniform shadow).  We could just set shadow properties on some of the sublayers, but that would involve a separate off-screen rendering pass, so we would need to also set the shadowPath appropriately and animate that.  A possible compromise improvement might be to use a CAGradientLayer (darkest by the edge of the covering page) for the reveal and facing page shadows and animate locations (or startPoint and endPoint).

Resources

  • MPFlipViewController – a fully touch gesture-enabled, attribution-licensed container controller, that uses all of the techniques discussed above.
  • MPFoldTransition project with demo app and code you are free to use to easily add these same animations to your own iOS apps.
  • Enter The Matrix project with lots of matrix transformation examples, including touch gesture-enabled versions of both flip and fold animations (the project has now been updated to incorporate the shadow techniques described above).
  • Edge Antialiasing post with a discussion of what it is, why you’d want to do it, and how to do it efficiently.  Also with its own sample project.
  • If you’re not doing WWDC this year, come see me speak about these types of things at CocoaConf DC (and learn from a great speaker lineup).

Credits

I’d like to thank Shawn Welch for starting me down the path of investigating page-flip animations at the Voices That Matter iOS conference in Boston last November, and providing a demo project to get me going.

MPFlipTransition – add flip transitions to your app


I’ve added flip transitions to my MPFoldTransition project on GitHub. It provides a class you can use to add a page-flipping transition to your application in just a single line of code (in most cases).

Update: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the page flips from right to left (Forward) or left to right (Backward).

Orientation


Sets whether the page flip is horizontal or vertical.

Perspective


Determines whether the page flips towards the user (Normal) or away from the user (Reverse).

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using flip transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    flipStyle:(MPFlipStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFlipStyle:(MPFlipStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  flipStyle:MPFlipStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFlipStyle:MPFlipStyleDirectionBackward 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite direction bit (Forward or Backward), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using flip transitions:

- (void)pushViewController:(UIViewController *)viewController
                 flipStyle:(MPFlipStyle)style;

- (UIViewController *)popViewControllerWithFlipStyle:
    (MPFlipStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    flipStyle:MPFlipStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFlipStyle:MPFlipStyleDirectionBackward];

Tip: pop your view controller using a style with the opposite direction bit (Forward or Backward) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFlipTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFlipStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFlipStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve, shadow effects, or skew), you can initialize your own instance of MPFlipTransition, set the properties as desired, and then call the perform: method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack flip segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFlipModalSegue, MPFlipNavPushSegue, or MPFlipNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only because I wrote it with ARC and included storyboard support.