How to Create an iOS Book Open Animation: Part 1
Learn how to create an iOS book open animation including page flips, with custom collection views layout and transitions. By Vincent Ngo.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Create an iOS Book Open Animation: Part 1
30 mins
Snapping to a Book
targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)
determines at which point the collection view should stop scrolling, and returns a proposed offset to set the collection view’s contentOffset
. If you don’t override this method, it just returns the default offset.
Add the following code after shouldInvalidateLayoutForBoundsChange(_:)
:
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// Snap cells to centre
//1
var newOffset = CGPoint()
//2
var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
//3
var width = layout.itemSize.width + layout.minimumLineSpacing
//4
var offset = proposedContentOffset.x + collectionView!.contentInset.left
//5
if velocity.x > 0 {
//ceil returns next biggest number
offset = width * ceil(offset / width)
} else if velocity.x == 0 { //6
//rounds the argument
offset = width * round(offset / width)
} else if velocity.x < 0 { //7
//removes decimal part of argument
offset = width * floor(offset / width)
}
//8
newOffset.x = offset - collectionView!.contentInset.left
newOffset.y = proposedContentOffset.y //y will always be the same...
return newOffset
}
Here's how you calculate the proposed offset for your book covers once the user lifts their finger:
- Create a new
CGPoint
callednewOffset
. - Grab the current layout of the collection view.
- Get the total width of a cell.
- Calculate the current offset with respect to the center of the screen.
- If
velocity.x > 0
, the user is scrolling to the right. Think ofoffset/width
as the book index you'd like to scroll to. - If
velocity.x = 0
, the user didn't put enough oomph into scrolling, and the same book remains selected. - If
velocity.x < 0
, the user is scrolling left. - Update the new x offset and return. This guarantees that a book will always be centered in the middle.
Build and run your app; scroll through them again and you should notice that the scrolling action is a lot snappier:
To finish up this layout, you need to create a mechanism to restrict the user to click only the book in the middle. As of right now, you can currently click any book regardless of its position.
Open BooksViewController.swift and place the following code under the comment // MARK: Helpers
:
func selectedCell() -> BookCoverCell? {
if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {
if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {
return cell
}
}
return nil
}
selectedCell()
will always return the middle cell.
Next, replace openBook(_:)
with the following:
func openBook() {
let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
vc.book = selectedCell()?.book
// UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handler
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.navigationController?.pushViewController(vc, animated: true)
return
})
}
This simply uses the new selectedCell
method you wrote rather than taking a book
as a parameter.
Next, replace collectionView(_:didSelectItemAtIndexPath:)
with the following:
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
openBook()
}
This simply removes the code that opened the book at the selected index; now you'll always open the book in the center of the screen.
Build and run your app; you'll notice now that the book in the center of the view is always the one that opens.
You're done with BooksLayout. It's time to make the on-screen book more realistic, and let the user flip the pages in the book!
Book Flipping Layout
Here's the final effect you're shooting for:
Now that looks more like a book! :]
Create a group named Layout under the Book group. Next, right-click the Layout folder and select New File..., then select the iOS\Source\Cocoa Touch Class template and click Next. Name the class BookLayout, make it a subclass of UICollectionViewFlowLayout, and set Language to Swift.
Just as before, your book collection view needs to use the new layout. Open Main.storyboard and select the Book View Controller Scene. Select the collection view and set the Layout to Custom. Finally, set the layout Class to BookLayout as shown below:
Open BookLayout.swift and add the following code above the BookLayout
class declaration:
private let PageWidth: CGFloat = 362
private let PageHeight: CGFloat = 568
private var numberOfItems = 0
You'll use these constant variables to set the size of every cell; as well, you're keeping track of the total number of pages in the book.
Next, add the following code inside the class declaration:
override func prepareLayout() {
super.prepareLayout()
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
numberOfItems = collectionView!.numberOfItemsInSection(0)
collectionView?.pagingEnabled = true
}
This is similar to what you did in BooksLayout
, with the following differences:
- Set the deceleration rate to
UIScrollViewDecelerationRateFast
to increase the rate at which the scroll view slows down. - Grab the number of pages in the current book.
- Enable paging; this lets the view scroll at fixed multiples of the collection view's frame width (rather than the default of continuous scrolling).
Still in BookLayout.swift, add the following code:
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
Again, returning true
lets the layout update every time the user scrolls.
Next, give the collection view a content size by overriding collectionViewContentSize()
as shown below:
override func collectionViewContentSize() -> CGSize {
return CGSizeMake((CGFloat(numberOfItems / 2)) * collectionView!.bounds.width, collectionView!.bounds.height)
}
This returns the overall size of the content area. The height of the content will always stay the same, but the overall width of the content is the number of items — that is, pages — divided by two multiplied by the screen's width. The reason you divide by two is that book pages are double sided; there's content on both sides of the page.
Just as you did in BooksLayout
, you need to override layoutAttributesForElementsInRect(_:)
so you can add the paging effect to your cells.
Add the following code just after collectionViewContentSize()
:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
//1
var array: [UICollectionViewLayoutAttributes] = []
//2
for i in 0 ... max(0, numberOfItems - 1) {
//3
var indexPath = NSIndexPath(forItem: i, inSection: 0)
//4
var attributes = layoutAttributesForItemAtIndexPath(indexPath)
if attributes != nil {
//5
array += [attributes]
}
}
//6
return array
}
Rather than calculating the attributes within this method like you did in BooksLayout
, you leave this task up to layoutAttributesForItemAtIndexPath(_:)
, as all cells are within the visible rect at any given time in the book implementation.
Here's a line by line explanation:
- Create a new array to hold
UICollectionViewLayoutAttributes
. - Loop through all the items (pages) in the collection view.
- For each item in the collection view, create an
NSIndexPath
. - Grab the attribute for the current
indexPath
. You'll overridelayoutAttributesForItemAtIndexPath(_:)
soon. - Add the attributes to your array.
- Return all the cell attributes.