Skip to content

Flip Controller

I was once asked to create an iOS Flipboard like controller. I immediately found the ask an interesting Core Animation exercise. In this post I’ll cover some of the concepts at a very high level that make the controller work. As far as “should one use flip or not in a mobile app” I will not favor one way or the other, that is not the purpose of this post. To be fair, I need to mention the great Flipboard app by McCue and Doll and its amazing success. If you are interested in getting a quick primer on the history and some very impressive stats about Flipboard I suggest you take a look at this article: I think Flipboard did a tremendous job at presenting digital content through this interaction, and yes, I am one of their 100 Million + users.

Before going into technical details I will give a quick intro at my personal experience as I started thinking about a possible solution.

First thing I did was researching online. Did anybody already create an open source solution I could simply use? Did it behave like Flipboard? I did find a bunch of projects that somewhat resembled the “Flip” like transition, but, none really gave me the same experience Flipboard delivered. Probably, the one thing I was not able to find was a solution that could flip as many pages at once as I liked. Each solution I found would only allow for one single flip transition at the time. Flipboard does allow you to flip multiple pages at the same time. That was the main trigger that got me to develop my own solution, and throughout development that has been one of the most interesting architectural problems to solve. There were other factors, like, the ability to flip through Views and ViewControllers at the same time making sure child view controller containment would work properly when the flip controller was fed view controllers for example, and many other small technicalities. And finally, I really wanted to create the first Flip Controller to be 100% written in Swift. I knew Swift was going to change dramatically, but, temptation was too high and since then I have transitioned the controller to the latest Swift versions as they came out.

Before jumping into some of the technical parts of the controller, you can play the short demo video of what it all looks like in test mode. I need to clarify that for the purpose of this demo I used much larger images than needed, simply to do a stress test, in-fact each image is between 2 Mb and 4 Mb. This video is recorded through quicktime on an iPhone 6+. Performance is amazing, and to be honest this video does not really make it justice. Nevertheless you can see how it handles single and concurrent flips. I slowed down the timing of the animations to make it easier to observe the transitions.

Technical Overview

To cover every detail of what makes this controller work would be too long for the purpose of this post, so I will just pick some core items.

First thing I wanted to make sure to use the proper Apple APIs to screenshot the views that would allow me to construct the layers that would then provide the flip effect.

Since iOS 7 Apple gave us a great support for taking screenshots. The API call I used is:

The docs state: “Returns a snapshot view based on the specified contents of the current view, with stretchable insets… calling this method is faster than trying to render the contents of the current view into a bitmap image yourself”.

This looked like the exact type of call I needed to construct the visual components that would then move on screen following a specific sequence that would allow a flip like transition for each page. It allows to specify a region on the view you want the snapshot to come from and it also allows asynchronous operation. After having worked with CGBitmapContextCreate for years I was really curious as to why this was dramatically faster after testing it. As I debugged the screenshot view given by the resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets: call I noticed that it’s backed by a UIReplicatorLayer.

I haven’t been able to find public docs on UIReplicatorLayer, just its header, which shows it’s a CALayer. Having used CAReplicator to make copies of CALayers in Core Animation very efficiently it makes sense to see this name while using the snapshotting API. Very fast approach and that works great for flipping.

Next, I needed to figure out how to break up the actual transition for each page. This sketch below tries to represent what went through my mind which I then implemented.

Flip transition steps

The flip animation needed a screenshot of the “fromView” and a screenshot of the “toView”. The screenshot for each view though had to be taken in halves. A top half and a bottom half. Depending on the direction of the flip transition these four different screenshots are then positioned accordingly and two of them are actually animated. To animate two of the screenshots I used a CATransformLayer. A CATransformLayer is used to create true 3D layer hierarchies. The end need was to be able to animate the two screenshots together, apply the needed 3D transforms to only the root container layer knowing that that transform would be propagated to the sublayers added. CATransformLayer is great for this and very light weight. Another thing to take into consideration was enabling the CATransformLayer to be doubleSided. That would allow both sides of the page in the flip transition to be visible as they were added to the transform layer with the proper Z index.
One of the screenshots also needed to be inverted before it was added to the moving container CATranformLayer. For the inversion I used a CGAffineTransform. The flip controller supports both vertical flipping and horizontal flipping. For example for vertical flip mode I would use:

A CGAffineTransform is really a struct that holds an affine transformation matrix. Here I took some time to leverage the information the mapping equations would give me during the animation.
The coordinates mapping equations:
mapping equations

The matrix:

The reason why I am going into some linear algebra here is because I wanted to have a way to determine the state of the animation, specifically the angle of the rotation as the TransformLayer changed. Without getting into the math, we can get the current angle of the animation by looking at the m11 matrix value on the CATransformLayer’s presentation layer. By taking the inverse cosine of the current transform matrix’s m11 value we’re able to know the exact radians and from there we can for example use that information to apply the correct shadow over the screenshots which make the flip effect look more real. One thing to note is the non-linear behavior of shadows between pages. For this I literally took a book, positioned it under my desk light and studied the behavior of the light in between pages as I turned the pages. It resembled an exponential behavior, so then I decided to map the angle of the transform to an alpha value using a parabolic equation.

Another very important value in the matrix is m34. That value provides the perspective to the whole Flip animation. So, for example I would set the desired perspective value like so:

The core Flip like 3D transform can be obtained by using a simple CATransform3DRotate:

I won’t go in details of the entire flip transform, but that alone will create the basic rotation around the X axis in case of a vertical flip. There are two ways the Flip Controller can generate the flip transition: programmatically or by tracking the user gesture on screen.
When the CATransformLayer moved on screen by tracking a touch event I ended up having to wrap the transform in a CATransaction. While moving the layer through a touch event I was getting a lag instead of having the layer update instantly following the touch. The reason was because the CALayer’s actions where triggered every time I would update its transform value. So I was getting animations within animations. Then setting:

allowed the layer’s transform to update immediately giving the proper feel and look while the TransformLayer was tracking the touch position.

As far as flipping pages programmatically I ended up using a CABasicAnimation, which through its delegate:

also allowed me to keep state consistent especially when flipping multiple pages concurrently.

Another object I ended up using has been a CADisplayLink object. As the angle of the transform changed I wanted to update the shadows on screen. I wanted to do so by synchronizing the drawing to the refresh rate of the display as to avoid calling update shadows too much or too little. The CADisplayLink object would then trigger the update, which in turn would read m11 in the matrix as explained above and determine the correct shadow amount to draw on screen.

Another interesting problem was handling the gesture just right. I implemented both a gesture recognizer and touches began, moved, etc…
Using the UIView touches overrides turned out to offer a quicker response. This is because a gesture recognizer has an initial delay (it has to recognize the gesture) before setting its state to .Began. However, I then subclassed the Gesture Recognizer importing UIKit.UIGestureRecognizerSubclass and wrote my own implementation which used less cycles of logic to mark the gesture as .Began. Why did I do that? Mainly because that allowed me to play around nicely with other gestures in the app and I was then able to use gesture delegates for example one being:

Another reason to use gestures has been to get for free things like gesture velocity:

This allowed to drive a gesture state I called “flick”. If the user reaches a certain velocity before ending / canceling the gesture then the page would “flick” and keep animating in the flick direction, just like a real page on a real book would.

As I stated earlier being able to flip multiple pages at the same time was a must for me. Initially I worked out the math to handle Z index logic so that all the screenshots on screen would play nicely with each other as far as their Z index went. I actually made that work by starting sketching something like this:

Multiple flip transitions on screen at once. Z index problem.
Multiple flip transitions on screen at once. Z index problem.

I won’t go into the math, but, after testing things out I realized a better approach and simplified things out. First, adding the layers on screen in the right order would automatically give me a way to handle Z indexes correctly. The only problem was when a page would be animating and the user would grab that layer during animation and change it’s direction. By doing so a page could overlap on another incoming page and that looked wrong obviously. The way I got around it was to set a minimum acceptable angle between pages (theta in the image), then even if the user would try to pull a page over another page the constraints would not allow that and things started working like expected. One thing to note is that this does not really happen most of the time. The user needs to flip very fast to get into that situation and capping the max concurrent flips on screen also prevents this from being likely to happen. But, if it does, it still works and I wanted it to be solid.

minimum angle between pages constraint
minimum angle between pages constraint

As I worked on all the intricacies of the animation, look and feel I wanted to make sure the API of the FlipController would be very easy to use. I also wanted to make sure that this controller could simply handle views but also view controllers.

If all you want to flip through are simple UIViews all I needed up having to implement around the app was this datasource call:

So for example if we’re wanting to flip views we would simply instantiate a FlipView and return views from that datasource call for the specified index.

I also had the need to flip views belonging to ViewControllers. For that I would then implement the following datasource call:

The FlipController then handles child view controller containment so that all the UIKit calls around view controllers would happen as expected and the responder chain would work properly even during multiple concurrent flip transitions.

Overall this has been a very interesting Core Animation problem to solve. Flip transitions put a heavier load on the app, taking screenshots and providing the needed views and or view controllers does increase the amount of work you have to do to support this interaction especially when compared to simple standard scrolling. So, if you decide to implement something like this in your app you’ll have to do some significant work to keep things balanced and keep the app performant. Just because it’s harder though it does not mean you can’t do it, it’s possible and I’ve personally obtained some very good performance results. Another thing to keep in mind is the hardware of the device you run this controller on. For example the maximum number of concurrent flip transitions change in this controller according to the device it runs on, so test your app well on as many devices as you can. This controller does a lot better on real devices than simulators. If you are interested into learning Core Animation I think this is a great exercise and I would highly suggest it. I have barely touched on some of the core concepts, going into details about many of the things I’ve outlined would take much much longer. Core Animation has always been one of my favorites Apple Frameworks, and after having developed on mobile devices for almost 20 years I can say Apple has really done something amazing in Core Animation. Platforms like Android and many other minor mobile platforms can only dream of having such a cool and mature framework to support their work.

Some of the capabilities of this controller are the following:

  • simple one flip transition
  • multiple concurrent flip transitions
  • programmatic auto-flip feature
  • handles both vertical and horizontal flip transitions
  • handles flip even during device rotation
  • 100% Swift
  • easy API
  • can flip UIViews or UIViewController’s views (handles child view controller containment)

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *