This tutorial will use the Leaves open-source project to build a simple iPad reader for The War of the Worlds by H.G. Wells. Along the way, we'll take a quick look at the code powering the Leaves project, discuss implementation details, and explore a few alternative options for achieving a similar effect.
About the Leaves Project
Leaves is an open-source iOS component that simulates a page turning transition between content views, much like the transition found in Apple's official iBooks application. The project was originally written by Tom Brow and made available on GitHub, where it was later forked by Ole Begemann to add support for multi-page layouts.
To see the Leaves project in action, take a look at the following video demo of the project this tutorial will teach you to create:
Of course, the page curling effect demonstrated above isn't unique to either this demo or the iBooks application. Other native iOS applications (such as the Maps app) also use a similar effect. For the full backstory on exactly how Apple achieves this in their applications and why iOS SDK developers need to resort to a component like Leaves, check out "App Store-Safe Page Curl Animations" by Ole Begemann and "Apple's iBooks Dynamic Page Curl" by Steven Troughton-Smith.
We'll come back to some of the theory behind the Leaves project later, but let's go ahead and jump into creating our War of the Worlds reader to get a feel for how the code looks in action.
Building the WOTW PDF Reader
Step 1: Download the Project Resources
The download file attached to this Mobiletuts+ post contains the Leaves source code and several public domain resources (including the War of the Worlds text) used in the project.
Step 2: Create a New Xcode Project
Open Xcode and create a new project using the "View-based Application" template. Name the project "WOTW" and select "iPad" from the device family drop-down.
Step 3: Add the Project Resources to Xcode
Drag-and-drop the following files into the "Supporting Files" group in Xcode:
- WOTW.pdf
- Icon.png
- [email protected]
All of the above files can be found in the "Resources" folder of the download attached to this tutorial.
Next, add the actual Leaves component code. Create a new group called "Leaves" under the "WOTW" folder in the Xcode project navigator, and then drag-and-drop the following files into the Leaves group:
- Utilities.h
- Utilities.m
- LeavesCache.h
- LeavesCache.m
- LeavesView.h
- LeavesView.m
- LeavesViewController.h
- LeavesViewController.m
NOTE: Be sure to select the "Copy items into destination group's folder" checkbox when adding the above files.
Step 4: Link the QuartzCore Framework
The animation created by Leaves is dependent on the QuartzCore Framework. Consequently, you'll need to link this framework against your project in order for Leaves to work. To do this in Xcode 4, start by selecting "WOTW" in the Project Navigator. Next, select the "WOTW" target and then the "Build Phases" tab. Then expand the "Link Binary With Libraries" drop down and click the plus symbol to add a new framework. Finally, select "QuartzCore" from the list of available frameworks and click "Add". The result of this process is visually displayed below:
Step 5: Subclass LeavesViewController
For our project, we want the reader to launch immediately into the War of the Worlds text without a lander page. To do this, we'll need to make the WOTWViewController
class inherit from the LeavesViewController
class instead of directly from UIViewController
. Do this by opening WOTWViewController.h and modifying the interface declaration to the following:
@interface WOTWViewController : LeavesViewController { }
Line 1 above modifies WOTWViewController
to inherit from LeavesViewController
instead of directly from UIViewController
. Because LeavesViewController
itself inherits from UIViewController
, you can think of the WOTWViewController
as the grandchild of UIViewController
So, what do we get for our trouble? LeavesViewController
conforms to the Leaves data source and delegate protocols (discussed later) and defines custom -loadView
and -viewDidLoad
implementations that add a special kind of UIView
called a LeavesView
to the current view hierarchy. The LeavesView
class is responsible for most of the work behind the page curl animation.
Step 6: Add Necessary Import Statements
In order for the WOTWViewController
class to be able to inherit from the LeavesViewController
class, we need to import the LeavesViewController
code. Add the following two #import
lines to WOTWViewController.h:
#import "Utilities.h" #import "LeavesViewController.h"
Wondering what that Utilities.h
import statement is for? As we'll see later, the Leaves project relies on a clever function declared in that file called aspectFit
. Many Cocoa-Touch projects maintain a Utilities class to declare and define helper code used throughout the application.
Step 7: Declare the WOTW Data Members
For now, we'll only need to declare a single data member for our project. In the WOTWViewController.h file, add the following line of code:
CGPDFDocumentRef bookPDF;
We will use "bookPDF" as a reference to the WOTW.pdf file later. The CGPDFDocumentRef
variable type will be used extensively in this tutorial and is worth further exploration in the official Apple documentation.
Step 8: Initialize Leaves
When the WOTWViewController
is created we need to initialize the bookPDF
data member declared above. As demonstrated in the Leaves sample project, you can do this with the following lines of code:
- (id)init { if (self = [super init]) { CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("WOTW.pdf"), NULL, NULL); bookPDF = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL); CFRelease(pdfURL); } return self; }
Do you see a problem with the approach above? Remember that we are going to be loading our view controller from Interface Builder.
The Leaves sample project assumes that you will be manually creating a LeavesViewController
instance programmatically by calling the -(id)init
method, like so:
WOTWViewController *viewController = [[[WOTWViewController alloc] init];
However, in our project we want to unarchive the WOTWViewController
from our Interface Builder NIB, so the custom -(id)init
function we just implemented will never be called. Instead, we need to provide a custom implementation for the -(id)initWithCoder:
method in order to plug into the NIB unarchive process and perform custom initialization. It's good to leave the init
in place so you have the option of creating this view controller manually, but we should create a new method for PDF initialization and call that method from both -(id)init
and -(id)initWithCoder:
.
Add the following line of code to the WOTWViewController.h file:
-(void)loadPDF;
Next, switch to WOTWViewController.m and implement the method as follows:
-(void)loadPDF { CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("WOTW.pdf"), NULL, NULL); bookPDF = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL); CFRelease(pdfURL); }
Finally, call the loadPDF
method from the class initializers:
- (id)init { self = [super init]; if (self) { [self loadPDF]; } return self; } -(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self loadPDF]; } return self; }
When our custom view controller is unarchived, the -(id)initWithCoder:
method will be called and will then call the loadPDF
method.
There's just one more thing. Having allocated memory for the bookPDF
reference, we should also release that memory when we're done with it. We can do this by adding the following line of code to the -(void)dealloc:
method:
CGPDFDocumentRelease(bookPDF);
Step 9: Implement the Leaves DataSource
Recall from Step 5 that the LeavesViewController
class automatically adds an instance of the LeavesView
class to the view hierarchy. The LeavesView
class somehow needs to find out what content should be displayed in the view, and this is achieved by calling two custom data source methods: -(NSUInteger)numberOfPagesInLeavesView:
and -(void)renderPageAtIndex:
.
To provide a custom implementation for these, open WOTWViewController.m and add the following lines of code:
#pragma mark LeavesViewDataSource methods - (NSUInteger) numberOfPagesInLeavesView:(LeavesView*)leavesView { return CGPDFDocumentGetNumberOfPages(bookPDF); } - (void) renderPageAtIndex:(NSUInteger)index inContext:(CGContextRef)ctx { CGPDFPageRef page = CGPDFDocumentGetPage(bookPDF, index + 1); CGAffineTransform transform = aspectFit(CGPDFPageGetBoxRect(page, kCGPDFMediaBox), CGContextGetClipBoundingBox(ctx)); CGContextConcatCTM(ctx, transform); CGContextDrawPDFPage(ctx, page); }
The -(NSUInteger)numberOfPagesInLeavesView:
method simply indicates how many pages of content the LeavesViewController
is responsible for displaying. If you were generating the content for Leaves manually or if you knew exactly how many pages were in the PDF, you could return that number manually here. Of course, it is always better to determine that number dynamically, and that is exactly what the CGPDFDocumentGetNumberOfPages()
function does.
The -(void)renderPageAtIndex:inContext:
method is responsible for actually drawing a CGContextRef
after first adding the PDF content into the context passed in as ctx
.
Testing the Code
Build and run the project. If everything goes well, you should now be able to see the War of the Worlds book cover and be able to flip through the pages with the page curl animation!
The final WOTWViewController.h file should look like this:
#import <UIKit/UIKit.h> #import <QuartzCore/QuartzCore.h> #import "Utilities.h" #import "LeavesViewController.h" @interface WOTWViewController : LeavesViewController { CGPDFDocumentRef bookPDF; } -(void)loadPDF; @end
And the final WOTWViewController.m file should read as follows:
#import "WOTWViewController.h" @implementation WOTWViewController #pragma mark - Initialization / Memory Management - (id)init { self = [super init]; if (self) { [self loadPDF]; } return self; } -(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self loadPDF]; } return self; } -(void)loadPDF { CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("WOTW.pdf"), NULL, NULL); bookPDF = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL); CFRelease(pdfURL); } - (void)dealloc { CGPDFDocumentRelease(bookPDF); [super dealloc]; } #pragma mark - LeavesViewDataSource methods - (NSUInteger) numberOfPagesInLeavesView:(LeavesView*)leavesView { return CGPDFDocumentGetNumberOfPages(bookPDF); } - (void) renderPageAtIndex:(NSUInteger)index inContext:(CGContextRef)ctx { CGPDFPageRef page = CGPDFDocumentGetPage(bookPDF, index + 1); CGAffineTransform transform = aspectFit(CGPDFPageGetBoxRect(page, kCGPDFMediaBox), CGContextGetClipBoundingBox(ctx)); CGContextConcatCTM(ctx, transform); CGContextDrawPDFPage(ctx, page); } @end
Alternatives to Leaves
In my research on Leaves I came across several additional open-source projects that add page-like transition animations. If you found this project interesting, you should also check out:
- FlipView - FlipBook like Animations
- HMGLTransitions - OpenGL Page Turns
- Paperstack - OpenGL (Unreleased as of 8/30/2011 - Hopefully Coming Soon!)
Leave a comment below if you'd like to see a tutorial on one of the above projects!
Do You Want to See More?
We've made some good progress in this tutorial. We can now interact with the War of the Worlds PDF and the page curling animation adds a great aesthetic feel to the eBook. However, there is still a lot of work to be done before this app would be ready for release on the App Store. For example, it would be great if the user was automatically returned to the last page they accessed when the app launches and it would also be really nice to have a UISlider
or similar interface component to quickly skip between sections of the book. Other useful features might include search, text highlighting, a table of contents, or bookmarks.
I want to make sure my iOS SDK tutorials cover topics that the Mobiletuts+ community is interested in. Do you think I should build out a full-fledged eReader and share my source code? Or perhaps you'd like to see a tutorial on something completely different? Answer the following poll and let me know!
UPDATE 9/7/2011: The poll is now closed. Over 60% of respondents expressed an interest in seeing more posts on adding a Table of Contents and/or a UISlider and a tutorial using a UISlider has been published (Link Below).
You can also send your feedback to my Twitter (@markhammonds) account, though I admit I normally only use Twitter when not working on freelance projects or writing tutorials, so you may just want to contact me via e-mail instead.
Comments