Introduction

Mobile Safari provides an alternative look to tabs - pages that line up neatly when you tap that right-most button. After that, you just flick left, flick right to choose the page you want, tap one more time and the page scales up to fill the screen. Beautiful interface. Too bad there is no easy-to-use UIViewController subclass from Apple to achieve the same interface.

Introducing... LSPageViewController!

In this article, I'll show you how to use LSPageViewController to mimic Mobile Safari's page switching interface in your own app, and how to customize that interface the way you want.

Prerequisites

Some knowledge of Objective-C. Not too much, but enough to write a HelloWorld app with a button that displays a smiling monkey when tapped.

Xcode with iPhone SDK 4.0 or newer. Tested on 4.0.

What I have learned along the way

During the journey of making this project, I put almost all of my focus on Core Animation, and my knowledge of implicit and explicit animations has been greatly improved. Also, I learned briefly about drawing custom content for a layer (the gradient background, the text and the dots) - and the fact that you need to call - setNeedsDisplay on every single layer with custom drawing methods, unlike UIViews. Finally, I learned about the great importance of UIViewControllers in an iPhone app.

New in 4.0: Blocks. Called "Lambda expressions" in C#, I believe. Blocks greatly simplified LSPagesPresentationView's implementation, since there is a + [CATransaction setCompletionBlock:] to replace the traditional callbacks and delegate methods (you'll find that - (void)animationDidStop:finished: is no longer implemented in LSPagesPresentationView). Of course, blocks are much more useful than that. I'd suggest you read Apple's documentation for more information.

Also, UIGestureRecognizer's concrete subclasses allow you to implement different gesture recognition algorithms for your view(s) without too much work.

Well, so much for the talking. Time to explore!

Tip of the iceberg

Grab yourself a copy of the source code, then open the project in Xcode. You can see that a number of classes reside in a group called "Main guts". Copy those classes over to your project and you're ready to use them. You'll be directly interacting with LSPageViewController only - LSPageViewController will manage the other classes.

The first thing to do is to make an instance of LSPageViewController. You can choose to add an instance of LSPageViewController to a nib file, create a separate nib file with an instance of LSPageViewController as the owner, or even do the work programmatically.

After that, you need to display LSPageViewController's view. You have several options here:

You can make LSPageViewController the main view controller by adding it to MainWindow.xib (or whatever NIB file you put your main window and application delegate in). Then, in your application delegate, you can configure LSPageViewController (setting frame, position, etc.) and add its view as a subview of the window. This approach is similar to the Navigation-based Application template in Xcode, where an UINavigationController is added to MainWindow.xib and hooked to the application delegate.

LSPageViewController's view can be a subview of another view controller (not the main window's subview). This solution is demonstrated in the example project. Please note that because LSPageViewController is not the main view controller, your main window will not forward some important messages to it (for example, - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration). Your main view controller is responsible for forwarding these messages. (For references, read the MainViewController class' implementation in the example project).

Create an instance of LSPageViewController programmatically, then add its view as a subview to a window or another view.

There are two ways to manipulate view controllers managed by LSPageViewController:

Call - [LSPageViewController addViewController:] to add new view controllers, - [LSPageViewController insertViewController:atIndex:] to insert a view controller to a specified location, and - [LSPageViewController removeViewControllerAtIndex:] to remove a view controller. You can also call - [LSPageViewController numberOfViewControllers] to check the number of view controllers managed by LSPageViewController.

Grab a reference to the NSMutableArray viewControllers that LSPageViewController uses to store view controllers by accessing the read-only property viewControllers. You can then add, insert, remove... however you like.

The difference between those two ways is that by calling LSPageViewController's methods, if the presentation view is being shown, LSPageViewController will have the chance to trigger necessary animations. When manipulating the NSMutableArray viewControllers directly, no animations will be shown regardless of the presence of the presentation view.

When you want to present all views for the user to pick (I call it the "presentation view"), invoke - [LSPageViewController displayPagesPresentationView].

And when you want to dismiss the aforementioned presentation view, just invoke - [LSPageViewController shouldDismissPagesPresenationView]

You can query the number of view controllers that the instance of LSPageViewController is managing by invoking - [LSPageViewController numberOfViewControllers]. To check if the presentation view is being shown, see LSPageViewController's isShowingAllView property.

A few notes:

You can assign an object to be LSPageViewController's delegate by accessing its delegate property. This object will be notified when the presentation view is successfully displayed (after invoking - [LSPageViewController displayPagesPresentationView]) and when the presentation view is dismissed (after calling - [LSPageViewController shouldDismissPresentationView]).

When the presentation view is successfully displayed, the delegate will receive a - (void)didShowPresentationView:(LSPagesPresentationView *)obj, and when the presentation view is dismissed, the delegate will receive a - (void)presentationViewDismissed.

Any UIViewController supplied to LSPageViewController should implement the method - (NSString *)presentationName - the string returned is displayed as the name of the view in the above screenshot. Alternatively, the UIView of the supplied UIViewController can implement that method. However, the UIViewController's method will be preferred over the UIView's (That means, if both implements - (NSString *)presentationName, the UIViewController's method will be called). If neither the UIViewController or the UIView of the UIViewController implement this method, the name will be set to "@Dev: name?".

You should make sure the UIViews managed by UIViewControllers that you supply to LSPageViewController are able to adapt to different sizes, because they will be resized to the same size as the LSPageViewController-managed view's. As an alternative, you can make sure both those views and the LSPageViewController-managed view are of the same size.

Also, you don't need to interact directly with LSPagesPresentationView. Ever. LSPageViewController will handle the work. You can, however, customize that class to change the behaviors and the look of the presentation view any way you like, as described in the following section.

New in 4.0: You can enable shadows to make the interface looks more pleasing by setting the useShadow property to YES. Note that by default, shadows are disabled since there could be a performance hit - although I'm not able to test this because I can't debug on my iPod. If anybody has access to the iPhone Developer Program, I will appreciate it if you try my project on your device(s) with shadows enabled to see if performance is affected.

Under the hood

LSPageViewController is just a regular subclass of UIViewController. Its mission is to handle adding and removing view controllers, and display and dismiss the presentation view when needed. When displaying the presentation view, it also has the mission of feeding the presentation view with CALayers representing the views to be shown to the users:

From the snippet, you can see that the layer is created by telling the view to draw into a custom context, grabbing an UIImage representation from that context - in short, grabbing a screenshot of the view programmatically, then assigning the content property of the CALayer to the image. The other 2 sections just add a close button and set the name to be shown to the user.

LSPagesPresentationView, on the other hand, is much more complex. When initiated, it receives an array populated with CALayers representing views. LSPageViewController, after setting proper values, sends the presentation view a - [LSPagesPresentationView setup]:

The first two setup methods are pretty self-explanatory. The third one creates and positions a container layer, and then set all view-representing layers to be sublayers of that container layer, while also setting their positions and bounds (relative to the container layer, of course). The fourth one essentially resizes the currently selected view's representative layer to fill the screen (actually, the root view) to prepare for the entrance (shrinking) animation.

After that, LSPageViewController just set the presentation view to be the only subview of its root view. Then, it calls - [LSPagesPresentationView startEntranceAnimation] and the currently selected view's layer shrinks into place, revealing the other views, the text and dot layers.

Animations can be triggered by touch events: taps and swipes recognized by gesture recognizers and other touch events that the recognizers couldn't recognize. Gesture recognizers are added to the instance of LSPagesPresentationView at initiation:

There is only one gesture recognizer added to the view, and it is a tap recognizer. This is a simplified list of actions that will occur when a touch event is generated for an instance of LSPagesPresentationView:

UIApplication sends a touch event to the front window.

The front window forwards that event to our view. But before receiving the event, the tap recognizer will process it first.

Our view then receives the event if the tap recognizer hasn't recognized (or failed to recognize) the gesture.

If the tap recognizer successfully recognized a gesture, - (void)touchesCancelled:forEvent will be called instead.

By reading the code, you'll see that - (void)touchesEnded:forEvent (which is only called if the tap recognizer officially fail to recognize a particular tap gesture) animate the layers to the next or previous location based on the movement of the finger. On the other hand, - (void)touchesCancelled:forEvent is either called when the tap recognizer recognizes a tap or when another action cancels the event (incoming phone call, for example) - and it deals with the two kinds of situation differently (doing nothing if the cancellation comes from the tap recognizer while animating to original location if the cancellation originated from something else).

All animations used in LSPagesPresentationView are implicit animations. Although they are implicit by nature, they can be very powerful. Here's an example of animations that will be triggered when an user taps the left area:

By simply assigning new values to various properties of the layers and grouping them inside + [CATransaction begin] and + [CATransaction commit], the layers will smoothly animate into their appropriate positions. Side note: since - (CGPoint)positionForLayerAtIndex: rely on selectedIndex, by updating selectedIndex, we will be able to generate appropriate positions for each and every layer managed by LSPagesPresentationView.

What about callbacks? What if you want to chain multiple animations together, or perform some tasks after the animations have completed? Prior to 4.0, you would have to make explicit CAAnimations, set the delegate property of one of those to self, and implement a - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)fin. Even worse, as you set up more and more animations with callbacks, your - animationDidStop:finished: will grow larger and larger. In the end, that delegate method becomes a huge tyrannosaurus that is bound to be unreadable by humans.

Luckily, in 4.0, as blocks are introduced, CATransaction sports a new class method: + [CATransaction setCompletionBlock:]. This replaces the need for clunky callbacks, and greatly simplify your code. Take a look at this snippet when you add a new view-representing layer to the instance of LSPagesPresentationView:

The bold lines show the completion block (encapsulated inside ^{ /* Code goes here... */ }) that will be executed right after the animation is finished. One special thing about block is that it inherits all the local variables declared prior to its declaration - although a block can only access a read-only representation of the variables. If you want a variable to be fully accessible by a block, you need to add the __block directive when declaring the variable (ex. __block int index).

Now LSPageViewController can respond to orientation change as well! If you use another view controller to manage LSPageViewController, you will need to forward - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration to the instance of LSPageViewController (again, look at the implementation of MainViewController in the example project). On the other hand, if LSPageViewController's view is added as a subview to the main window, there's no need to forward the message. However, in both cases, you need to configure the autoresizing mask of LSPageViewController's view in order for it to resize correctly when the orientation changes - this configuration can be done both in Interface Builder or programmatically.

To be more specific, when the orientation changes, LSPageViewController receives a message (either forwarded from your own view controller or automatically from the main window; the name of the message is - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration - obviously.) LSPageViewController then proceeds to resize the views of the view controllers that it manages, and if the presentation view is being shown, resize it and notifies it of the orientation change so it can resize and reposition its layers (in LSPagesPresentationView's -(void)respondtoOrientationChange:).

In summary, this section showed you how LSPageViewController passes layers to the instance of LSPagesPresentationView, how the layers are set up and animated into position after being passed to LSPagesPresentationView, how gesture recognizers are set up and how implicit animations are used in the project.

Now that you have understood the underpinnings of LSPageViewController and LSPagesPresentationView, you can easily customize those two to fit with your requirements.

Make it yours

You want more labels? No labels? Change background? No worries! You can customize LSPageViewController and LSPagesPresentationView as much as possible, until they feel just right for you.

There isn't much need to change LSPageViewController, because it only serves as the middle man between your view controller and LSPagesPresentationView - storing the supplied UIViewControllers and passing them to LSPagesPresentationView when - [LSPageViewController displayPagesPresentationView] is called. However, as mentioned above, LSPageViewController does not pass raw view controllers - but processed CALayers - to LSPagesPresentationView, if you need to customize those layers, read and change - (CALayer *)layerForViewController:(UIViewController *)vc to suit your needs.

There are a lot more options for customizing LSPagesPresentationView - this is where the magic happens.

To customize the background gradient (change the colors or draw a completely different one), modify - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx in the Delegate category.

For customization regarding size and position of layers, refer to the category Maths in LSPagesPresentationView.m. Read LSPagesPresentationViewPrivate.m to see a list of Maths method. Be aware that many Maths methods are dependent on other methods so modifying one can affect them all. In general, study all Maths method before customizing.

To customize name layers, refer to - (void)setupNameLayer. From here, you can add more or delete all of them. If you change this method, also check these methods:

- (void)prepareForEntranceAnimation

- (void)handleSingleTap

- (void)handleSwipeLeft

- (void)handleSwipeRight

- (void)touchesBegan:forEvent:

- (void)touchesMoved:forEvent:

- (void)touchesEnded:forEvent:

- (void)touchesCancelled:forEvent:

- (void)addNewLayer:

- (void)insertLayer:atIndex:

- (void)removeLayerAtIndex:

The methods above contain animations that will move the view-representing layers and also update the dot and name layers to match currentLayer. You need to change these methods to accommodate the addition or removal of name layers.

To customize animations, check the aforementioned list of methods. LSPagesPresentationView no longer has a dedicated Animation category, because I found that solution to be too inflexible - there are small tweaks needed for each animation. If you want to be absolutely sure, just search for "[CATransaction begin]" in the .m file. Each [CATransation begin/commit] group represents a group of implicit animations.

In this new version of LSPagesPresentationView, there is also no longer a - (void)processTouch:. Gesture recognizers now handle the work for us. There is still an NSMutableDictionary extraTouchInfo but that is only used to store information to be used among the EventHandling methods - it is insignificant compared to the role of touchStorage in the previous version (for 3.1.3 and lower). If you want to modify the gesture recognition algorithm, make subclasses of UIGestureRecognizer and replace the 3 stock recognizers I used in - (id)initWithFrame:layers:. You can consult Apple's documentation about subclassing notes for UIGestureRecognizer.

That covered pretty much everything. Feel free to shout out if you have any questions or concern.

Thanks & Acknowledgements

Scott Stevenson. His ArtGallery project was a great help. Although I didn't directly use any piece of code from the project, it gave me an idea on how to implement this.

Anybody kind enough to test this project on their own devices, as I cannot confirm the performance of the library solely with the iPhone Simulator.

And many more important figures whom I have forgotten.

To do

Currently satisfied (again). Let me know if you want something improved or included.

Still need to test this on a real device though :(

History

12 July 2010: Now supports orientation changes! Requires the user of this library to comply to some requirements though - read the sections above.

30 June 2010: Removed two swipe gesture recognizers and made slight changes to animation behaviors - there is a 'friction' effect if you try to move a leftmost layer to the right, or the rightmost layer to the left, where there is no more layer to display.

27 June 2010: Added - [LSPageViewController insertViewController:atIndex:] for those who might need it. Also, the NSMutableArray that LSPageViewController uses to store managed view controllers is now exposed through the read-only property viewControllers (read-only means that you can't assign a new array to that property - you can still add and remove objects at will).

25 June 2010: Support for iOS 4.

UIGestureRecognizer subclasses are used instead of manual gesture recognition, hence the removal of touchStorage and - (void)processTouch:.

When chaining animations, callbacks are no longer used. Instead, blocks are used by assigning them with + [CATransaction setCompletionBlock:]. The assigned blocks will then be executed when all animations inside a CATransaction group have finished. The codebase is greatly simplified and easier to read.

Shadows can be enabled, but I'm not sure whether there is any performance hit. (If anybody is able to test this on their own device, sound off in the comments)

It is okay and sometimes even necessary to be verbose. Not only can the articles be helpful, but so can the feedback on articles. Simply stating that an article is good, but not providing a good score is not anymore helpful than an article that doesn't provide enough detailed or accurate information. Perhaps it would be good for both the authors and the readers to know what you liked about the article and specifically what it trite and, seemingly, already done. We are all suppose to be here learning.

Would you mind stating your reason for giving this article a vote of 1? Between myself finding Tom The Cat's article to be pretty good and the lack of a stated reason for the vote or any information on how you feel the article could be improved I've just voted to have your vote of 1 removed from Tom the Cat's article.

I agree with Joel Ivory Johnson, you made a vote of 1, yet your comment consist of what, 1 letter? Unless you could state the reason behind the vote and ideas on how to improve it, I'm voting to have this removed both from here and from another article that you voted down - that time, with a more generous 2 letter comment

Now I know what the geeks and nerds in high school have to go through every day.

Some one once suggested I do the same thing with another article some time ago. That's when I came to realize that no one on my Facebook friend list has development/technical interest. Yet many of them think most of my other friends are technical.