Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all 4370 articles
Browse latest View live

iBeacons Tutorial with iOS and Swift

$
0
0
Use Core Location and iBeacons to track distances from your phone!

Use Core Location and iBeacons to track distances from your phone!

Update note: This tutorial was updated for iOS 8, Swift 1.2 and Xcode 6.3 by Adrian Strahan. Original post by Tutorial Team member Chris Wagner.

Have you ever wished that your phone could show your location inside a large building like a shopping mall or baseball stadium?

Sure, GPS can give you an idea of which side of the building you are in. But good luck getting an accurate GPS signal in one of those steel and concrete sarcophaguses. What you need is something inside of the building to let your device determine its physical location.

Enter iBeacons! In this iBeacons tutorial you’ll create an app that lets you register known iBeacon emitters and tells you when your phone has moved outside of their range. The use case for this app is attaching an iBeacon emitter to your laptop bag, purse, or even your cat’s collar — anything important you don’t want to lose. Once your device moves outside the range of the emitter, your app detects this and notifies you.

To continue with this tutorial, you’ll need to test on a real iOS device and an iBeacon. If you don’t have an iBeacon but have a second iOS device, you might be able to use it as a beacon; read on!

Getting Started

There are many iBeacon devices available; a quick Google search should help reveal them to you. But when Apple introduced iBeacon, they also announced that any compatible iOS device could act as an iBeacon. The list currently includes the following devices:

  • iPhone 4s or later
  • 3rd generation iPad or later
  • iPad Mini or later
  • 5th generation iPod touch or later

Note: If you do not have a standalone iBeacon emitter but you do have another iOS device that supports iBeacons, you can follow along by creating an app that acts as an iBeacon as described in Chapter 22 — What’s new in Core Location of iOS 7 by Tutorials.

An iBeacon is nothing more than a Bluetooth Low Energy device that advertises information in a specific structure. Those specifics are beyond the scope of this tutorial, but the important thing to understand is that iOS can monitor for iBeacons that emit three values known as: UUID, major and minor.

UUID is an acronym for universally unique identifier, which is a 128-bit value that’s usually shown as a hex string like this: B558CBDA-4472-4211-A350-FF1196FFE8C8. In the context of iBeacons, a UUID is generally used to represent your top-level identity.

Major and minor values provide a little more granularity on top of the UUID. These values are simply 16 bit unsigned integers that identify each individual iBeacon, even ones with the same UUID.

For instance, if you owned multiple department stores you might have all of your iBeacons emit the same UUID, but each store would have its own major value, and each department within that store would have its own minor value. Your app could then respond to an iBeacon located in the shoe department of your Miami, Florida store.

ForgetMeNot Starter Project

Download the starter project here — it contains a simple interface for adding and removing items from a table view. Each item in the table view represents a single iBeacon emitter, which in the real world translates to an item that you don’t want to leave behind.

Build and run the app; you’ll see an empty list, devoid of items. Press the + button at the top right to add a new item as shown in the screenshot below:

First Launch

To add an item, you simply enter a name for the item and the values corresponding to its iBeacon. You can find your iBeacon’s UUID by reviewing your iBeacon’s documentation – try adding it now, or use some placeholder values, as shown below:

Add an Item

Press Save to return to the list of items; you’ll see your item with a location of Unknown, as shown below:

List of Items Added

You can add more items if you wish, or swipe to delete existing ones. NSUserDefaults persists the items in the list so that they’re available when the user re-launches the app.

On the surface it appears there’s not much going on; most of the fun stuff is under the hood. The unique aspect in this app is the Item model class which represents the items in the list.

Open Item.swift and have a look at it in Xcode. The model class mirrors what the interface requests from the user, and it conforms to NSCoding so that it can be serialized and deserialized to disk for persistence.

Now take a look at AddItemViewController.swift. This is the controller for adding a new item. It’s a simple UITableViewController, except that it does some validation on user input to ensure that the user enters valid names and UUIDs.

The Save button at the top right becomes tappable as soon as nameTextField and uuidTextField are both valid.

Now that you’re acquainted with the starter project, you can move on to implementing the iBeacon bits into your project!

Core Location Permissions

Your device won’t listen for your iBeacon automatically — you have to tell it to do this first. The CLBeaconRegion class represents an iBeacon; the CL class prefix infers that it is part of the Core Location framework.

It may seem strange for an iBeacon to be related to Core Location since it’s a Bluetooth device, but consider that iBeacons provide micro-location awareness while GPS provides macro-location awareness. You would leverage the Core Bluetooth framework for iBeacons when programming an iOS device to act as an iBeacon, but when monitoring for iBeacons you only need to work with Core Location.

Your first order of business is to adapt the Item model for CLBeaconRegion.

Open Item.swift and add the following import to the top of the file:

import CoreLocation

Next, change the majorValue and minorValue definitions as well as the initializer as follows:

let majorValue: CLBeaconMajorValue
let minorValue: CLBeaconMinorValue
 
init(name: String, uuid: NSUUID, majorValue: CLBeaconMajorValue, minorValue: CLBeaconMinorValue) {
  self.name = name
  self.uuid = uuid
  self.majorValue = majorValue
  self.minorValue = minorValue
}

CLBeaconMajorValue and CLBeaconMinorValue are both a typealias for UInt16, and are used for representing major and minor values in the CoreLocation framework.

Although the underlying data type is the same, this improves readability of the model and adds type safety so you don’t mix up major and minor values.

Open ItemsViewController.swift, add the Core Location import to the top of the file:

import CoreLocation

Add the following property to the ItemsViewController class:

let locationManager = CLLocationManager()

You’ll use this CLLocationManager instance as your entry point into Core Location.

Next, replace viewDidLoad() with the following:

override func viewDidLoad() {
  super.viewDidLoad()
 
  locationManager.requestAlwaysAuthorization()
 
  loadItems()
}

The call to requestAlwaysAuthorization() will prompt the user for access to location services if they haven’t granted it already. Always and When in Use are new variants on location permissions in iOS 8. When the user grants Always authorization to the app, the app can start any of the available location services while it is running in the foreground or background.

Since this tutorial will be covering “Region” monitoring aspect of iBeacons, you’ll need the Always location permissions scope for triggering region events while the app is both in the foreground and background.

iOS 8 requires that you set up a string value in Info.plist that will be displayed to the user when access to their location is required by the app. If you don’t set this up, location services won’t work at all — you don’t even get a warning!

Open Info.plist and add a new entry by clicking on the + that appears when you select the Information Property List row.

plistwithoutentry

Unfortunately, the key you need to add isn’t in the pre-defined list shown in the dropdown list of keys — so you need to type the key in directly. Call the key NSLocationAlwaysUsageDescription and make sure the Type is set to String. Then add the phrase you want to show to the user to tell them why you need location services on, for example: “ForgetMeNot would like to teach you how to use iBeacons!”.

plistwithentry

Build and run your app; once running, you should be shown a message asking you to allow the app access to your location:

allowlocation

Allowing access to your location

Select ‘Allow’, and the app will be able to track your iBeacons.

Listening for Your iBeacon

Now that your app has the location permissions it needs, it’s time to find those beacons! Add the following class extension to the bottom of ItemsViewController.swift :

// MARK: - CLLocationManagerDelegate
 
extension ItemsViewController: CLLocationManagerDelegate {
}

This will declare ItemsViewController as conforming to the CLLocationManagerDelegate protocol. You’ll add the delegate methods inside this extension to keep them nicely grouped together.

Next, add the following line to the end of viewDidLoad():

locationManager.delegate = self

This sets the CLLocationManager delegate to self so you’ll receive delegate callbacks.

Now that you have an instance of CLLocationManager, you can instruct your app to begin monitoring for specific regions using CLBeaconRegion. When you register a region to be monitored, those regions persist between launches of your application. This will be important later when you respond to the boundary of a region being crossed while your application is not running.

Your iBeacon items in the list are represented by the the Item model via the items array property. CLLocationManager, however, expects you to provide a CLBeaconRegion instance in order to begin monitoring a region.

In ItemsViewController.swift create the following helper method in the ItemsViewController class:

func beaconRegionWithItem(item:Item) -> CLBeaconRegion {
  let beaconRegion = CLBeaconRegion(proximityUUID: item.uuid,
                                            major: item.majorValue,
                                            minor: item.minorValue,
                                       identifier: item.name)
  return beaconRegion
}

This returns a new CLBeaconRegion instance derived from the provided Item.

You can see that the classes are similar in structure to each other, so creating an instance of CLBeaconRegion is very straightforward since it has direct analogs to the UUID, major value, and minor value.

Now you need a method to begin monitoring a given item. Add the following method to ItemsViewController next:

func startMonitoringItem(item: Item) {
  let beaconRegion = beaconRegionWithItem(item)
  locationManager.startMonitoringForRegion(beaconRegion)
  locationManager.startRangingBeaconsInRegion(beaconRegion)
}

This method takes an Item instance and creates a CLBeaconRegion using the method you defined earlier. It then tells the location manager to start monitoring the given region, and to start ranging iBeacons within that region.

Ranging is the process of discovering iBeacons within the given region and determining their distance. An iOS device receiving an iBeacon transmission can approximate the distance from the iBeacon. The distance (between transmitting iBeacon and receiving device) is categorized into 3 distinct ranges:

  • Immediate Within a few centimeters
  • Near Within a couple of meters
  • Far Greater than 10 meters away
Note: The real distances for Far, Near, and Immediate are not specifically documented, but this Stack Overflow Question gives a rough overview of the distances you can expect.

By default, monitoring notifies you when the region is entered or exited regardless of whether your app is running. Ranging, on the other hand, monitors the proximity of the region only while your app is running.

You’ll also need a way to stop monitoring an item’s region after it’s deleted. Add the following method to ItemsViewController:

func stopMonitoringItem(item: Item) {
  let beaconRegion = beaconRegionWithItem(item)
  locationManager.stopMonitoringForRegion(beaconRegion)
  locationManager.stopRangingBeaconsInRegion(beaconRegion)
}

The above method reverses the effects of startMonitoringItem(_:) and instructs the CLLocationManager to stop monitor and ranging activities.

Now that you have the start and stop methods, it’s time to put them to use! The natural place to start monitoring is when a user adds a new item to the list.

Have a look at saveItem(_:) in ItemsViewController.swift. This unwind segue is called when the user hits the Save button in the AddItemViewController and creates a new Item to monitor. Find the call to persistItems() in that method and add the following line just before it:

startMonitoringItem(newItem)

That will activate monitoring when the user saves an item. Likewise, when the app launches, the app loads persisted items from NSUserDefaults, which means you have to start monitoring for them on startup too.

In ItemsViewController.swift, find loadItems() and add the following line inside the inner for loop:

startMonitoringItem(item)

This will ensure each item is being monitored.

Now you need to take care of removing items from the list. Find tableView(_:commitEditingStyle:forRowAtIndexPath:) and add the following line after itemToRemove is declared:

stopMonitoringItem(itemToRemove)

This table view delegate method is called when the user deletes the row. The existing code handles removing it from the model and the view, and the line of code you just added will also stop the monitoring of the item.

At this point you’ve made a lot of progress! Your application now starts and stops listening for specific iBeacons as appropriate.

You can build and run your app at this point; but even though your registered iBeacons might be within range your app has no idea how to react when it finds one…time to fix that!

Acting on Found iBeacons

Now that your location manager is listening for iBeacons, it’s time to react to them by implementing some of the CLLocationManagerDelegate methods.

First and foremost is to add some error handling, since you’re dealing with very specific hardware features of the device and you want to know if the monitoring or ranging fails for any reason.

Add the following two methods to the CLLocationManagerDelegate class extension you defined earlier at the bottom of ItemsViewController.swift:

func locationManager(manager: CLLocationManager!, monitoringDidFailForRegion region: CLRegion!, withError error: NSError!) {
  println("Failed monitoring region: \(error.description)")
}
 
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
  println("Location manager failed: \(error.description)")
}

These methods will simply log any received errors as a result of monitoring iBeacons.

If everything goes smoothly in your app you should never see any output from these methods. However, it’s possible that the log messages could provide very valuable information if something isn’t working.

The next step is to display the perceived proximity of your registered iBeacons in real-time. Add the following stubbed-out method to the CLLocationManagerDelegate class extension:

func locationManager(manager: CLLocationManager!, didRangeBeacons beacons: [AnyObject]!, inRegion region: CLBeaconRegion!) {
  if let beacons = beacons as? [CLBeacon] {
    for beacon in beacons {
      for item in items {
        // TODO: Determine if item is equal to ranged beacon
      }
    }
  }
}

This delegate method is called when iBeacons come within range, move out of range, or when the range of an iBeacon changes.

The goal of your app is to use the array of ranged iBeacons supplied by the delegate methods to update the list of items and display their perceived proximity. You’ll start by iterating over the beacons array, and then iterating over items to see if there are matches between in-range iBeacons and the ones in your list. You’ll come back to the TODO section in just a moment.

Open Item.swift and add the following property to the Item class:

dynamic var lastSeenBeacon: CLBeacon?

This property stores the last CLBeacon instance seen for this specific item, which is used to display the proximity information. The property has a dynamic modifier so that you can use it with key-value observation later on.

Now add the following equality operator at the bottom of the file, outside the class definition:

func ==(item: Item, beacon: CLBeacon) -> Bool {
  return ((beacon.proximityUUID.UUIDString == item.uuid.UUIDString)
    && (Int(beacon.major) == Int(item.majorValue))
    && (Int(beacon.minor) == Int(item.minorValue)))
}

This equality function compares a CLBeacon instance with an Item instance to see if they are equal — that is, if all of their identifiers match. In this case, a CLBeacon is equal to an Item if the UUID, major, and minor values are all equal.

Now you’ll need to complete the ranging delegate method with a call to the above helper method. Open ItemsViewController.swift and return to locationManager(_:didRangeBeacons:inRegion:). Replace the TODO comment in the innermost for loop with the following:

if item == beacon {
  item.lastSeenBeacon = beacon
}

Here, you set lastSeenBeacon when you find a matching item and iBeacon. Checking that the item and beacon match is easy thanks to your equality operator!

Now it’s time to use that property to display the perceived proximity of the ranged iBeacon.

Open ItemCell.swift and add the following to the beginning of the didSet property observer for item:

item?.addObserver(self, forKeyPath: "lastSeenBeacon", options: .New, context: nil)

When you set the item for the cell you’re also adding an observer for the lastSeenBeacon property. To keep things properly balanced, you also need to remove the observer if the cell already had an item set, as required by key-value observation. Add a willSet property observer next to didSet. Make sure it’s still inside the item property:

willSet {
  if let thisItem = item {
    thisItem.removeObserver(self, forKeyPath: "lastSeenBeacon")
  }
}

This will ensure there’s only one thing being observed.

You should also remove the observer when the cell is deallocated. Still in ItemCell.swift, add the following deinitializer to the ItemCell class:

deinit {
  item?.removeObserver(self, forKeyPath: "lastSeenBeacon")
}

Now that you’re observing for the value, you can put some logic in to react to any changes in the iBeacon’s proximity.

Each CLBeacon instance has a proximity property which is an enum with values of Far, Near, Immediate, and Unknown.

In ItemCell.swift, add an import statement for CoreLocation:

import CoreLocation

Next, add the following method to the ItemCell class:

func nameForProximity(proximity: CLProximity) -> String {
  switch proximity {
  case .Unknown:
    return "Unknown"
  case .Immediate:
    return "Immediate"
  case .Near:
    return "Near"
  case .Far:
    return "Far"
  }
}

This returns a human-readable proximity value from proximity which you’ll use next.

Now add the following method:

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
  if let anItem = object as? Item where anItem == item && keyPath == "lastSeenBeacon" {
    let proximity = nameForProximity(anItem.lastSeenBeacon!.proximity)
    let accuracy = String(format: "%.2f", anItem.lastSeenBeacon!.accuracy)
    detailTextLabel!.text = "Location: \(proximity) (approx. \(accuracy)m)"
  }
}

You call the above method each time the lastSeenBeacon value changes, which sets the cell’s detailTextLabel.text property with the perceived proximity value and approximate ‘accuracy’ taken from the CLBeacon.

This latter value may fluctuate due to RF interference even when your device and iBeacon are not moving, so don’t rely on it for a precise location for the beacon.

Now ensure your iBeacon is registered and move your device closer or away from your device. You’ll see the label update as you move around, as shown below:

Your cat’s so close!

You may find that the perceived proximity and accuracy is drastically affected by the physical location of your iBeacon; if it is placed inside of something like a box or a bag, the signal may be blocked as the iBeacon is a very low-power device and the signal may easily become attenuated.

Keep this in mind when designing your application — and when deciding the best placement for your iBeacon hardware.

Notifications

Things feel pretty complete at this point; you have your list of iBeacons and can monitor their proximity in real time. But that isn’t the end goal of your app. You still need to notify the user when the app is not running in case they forgot their laptop bag or their cat ran away — or worse, if their cat ran away with the laptop bag! :]

zorro-ibeacon

They look so innocent, don’t they?

At this point, you’ve probably noticed it doesn’t take much code to add iBeacon functionality to your app. Adding a notification when a cat runs away with your laptop bag is no different!

Open AppDelegate.swift and add the following import:

import CoreLocation

Next, make the AppDelegate class conform to the CLLocationManagerDelegate protocol by adding the following to the very bottom of AppDelegate.swift (below the closing brace):

// MARK: - CLLocationManagerDelegate
extension AppDelegate: CLLocationManagerDelegate {
}

Just as before, you need to initialize the location manager and set the delegate accordingly.
Add a new locationManager property to the AppDelegate class, initialized with an instance of CLLocationManager:

let locationManager = CLLocationManager()

Then add the following statement to the very top of application(_:didFinishLaunchingWithOptions:):

locationManager.delegate = self

Recall that any regions you add for monitoring using startMonitoringForRegion(_:) are shared by all location managers in your application. So the final step here is simply to react when Core Location wakes up your app when a region is encountered.

Add the following method to the class extension you added at the bottom of AppDelegate.swift, like so:

func locationManager(manager: CLLocationManager!, didExitRegion region: CLRegion!) {
  if let beaconRegion = region as? CLBeaconRegion {
    var notification = UILocalNotification()
    notification.alertBody = "Are you forgetting something?"
    notification.soundName = "Default"
    UIApplication.sharedApplication().presentLocalNotificationNow(notification)
  }
}

Your location manager calls the above method when you exit a region, which is the event of interest for this app. You don’t need to be notified if you move closer to your laptop bag — only if you move too far away from it.

Here you check the region to see if it’s a CLBeaconRegion, since it’s possible it could be a CLCircularRegion if you’re also performing geolocation region monitoring. Then you post a local notification with the generic message “Are you forgetting something?“.

In iOS 8 and later, apps that use either local or remote notifications must register the types of notifications they intend to deliver. The system then gives the user the ability to limit the types of notifications your app displays. The system does not badge icons, display alert messages, or play alert sounds if any of these notification types are not enabled for your app, even if they are specified in the notification payload.

Add the following to the top of of application(_:didFinishLaunchingWithOptions:):

let notificationType:UIUserNotificationType = UIUserNotificationType.Sound | UIUserNotificationType.Alert
let notificationSettings = UIUserNotificationSettings(forTypes: notificationType, categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)

This simply says that the app wishes to display an alert and play a sound when it receives a notification.

Build and run your app; make sure that your app can see one of your registered iBeacons and put the app into the background by pressing the Home button — which is a real-world scenario given that you want the app to notify you whilst you’re pre-occupied with something else — perhaps another Ray Wenderlich tutorial app? :]. Now move away from the iBeacon and once you’re far enough away you’ll see the notification pop up:

Notification on lock screen

Note: Apple delays exit notifications in undocumented ways. This is probably by design so that your app doesn’t receive premature notifications if you’re loitering on the fringe of the range or if the iBeacon’s signal is briefly interrupted. In my experience, the exit notification usually occurs up to a minute after the iBeacon is out of range.

Where to Go From Here?

Didn’t tie an iBeacon to your source code? You can download the final project here, with everything you’ve done in this tutorial.

You now have a very useful app for monitoring those things that you find tricky to keep track of. With a bit of imagination and coding prowess you could add a lot of really useful features to this app:

  • Notify the user which item has moved out of range.
  • Repeat the notification to make sure the user sees it.
  • Alert the user when iBeacon is back in range.

This iBeacons tutorial merely scratches the surface of what’s possible with iBeacons.

iBeacons aren’t just limited to custom apps; you can use them with Passbook passes as well. If you ran a movie theater, for example; you could offer movie tickets as Passbook passes. When patrons walked up to the ticket taker with an iBeacon nearby, their app would present the ticket on their iPhone automatically!

If you have any questions or comments on this tutorial, or if you have any novel ideas for the use of iBeacons, feel free to join the discussion below!

The post iBeacons Tutorial with iOS and Swift appeared first on Ray Wenderlich.


Video Tutorial: What’s New in watchOS 2 Part 7: Conclusion

Video Tutorial: Introducing iOS 9 Search APIs: Series Introduction

Video Tutorial: Introducing iOS 9 Search APIs Part 1: Activity Searching

How to Create an iOS Book Open Animation: Part 1

$
0
0
Learn how to create a cool book opening animation!

Learn how to create a cool book opening animation!

In this two-part tutorial series, you’ll develop a nifty iOS book open animation and page flip animation similar to Paper by FiftyThree:

  • In Part 1 you’ll learn how to customize your collection view layouts and apply depth and shadow to make the app look realistic.
  • In Part 2, you’ll learn to create custom transitions between different controllers in a sensible way and integrate gestures to create natural, intuitive transitions between views.

This tutorial is for intermediate to advanced developers; you’ll be working with custom transitions and custom collection view layouts.

If you’ve never worked with a collection view before, start with some of our other iOS tutorials first.

Note: Full credit goes to Attila Hegedüs for creating this awesome sample project.

Getting Started

Download the starter project for this tutorial here; extract the contents of the zip file, and open Paper.xcodeproj in Xcode.

Build and run the project in the simulator; you’ll see the following:

VN_paperAnimation2

The app is pretty much fully built; you can scroll through your library of books and select one of your favorite books to view. But when was the last time you read a book which had its pages side-by-side? With a bit of UICollectionView know-how, you can dress up the page view quite a bit!

The Project Structure

Here’s a quick rundown of the most important bits of the starter project:

The Data Models folder contains three files:

  • Books.plist contains sample book data. Each book contains an image cover along with an array of images to represent pages.
  • BookStore.swift is a singleton that is only created once in the life cycle of the app. The BookStore’s job is to load data from Books.plist and create Book objects.
  • Book.swift is a class that stores information related to the book, such as retrieving the book cover, the image for each page index, and the number of pages.

The Books folder contains two files:

  • BooksViewController.swift is a subclass of UICollectionViewController. This class is responsible for displaying your list of books horizontally.
  • BookCoverCell.swift displays all your book covers; it’s used by BooksViewController.

In the Book folder you’ll find the following:

  • BookViewController.swift is also a subclass of UICollectionViewController. Its purpose is to display the pages of the book when you select a book from BooksViewController.
  • BookPageCell.swift is used by BookViewController to display all the pages in a book.

Here’s what’s in the last folder, Helpers:

  • UIImage+Helpers.swift is an extension for UIImage. The extension contains two utility methods, one to round the corners of an image, and another to scale an image down to a given size.

That’s all! Enough of the review — it’s time to lay down some code!

Customizing the Book Layout

First you need to to override the default layout for BooksViewController‘s collection view. The existing layout shows three big book covers that takes up the whole screen. You’ll scale it down a bit to make it look more pleasant, like so:

VN_AnimationBooksScrolling

As you scroll, the cover image nearest the center of the screen grows in size to indicate it’s the active selection. As you keep scrolling, the book cover shrinks in size to indicate you’re setting it aside.

Create a group named Layout under the App\Books 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 BooksLayout, make it a subclass of UICollectionViewFlowLayout, and set Language to Swift.

Next you need to instruct BooksViewController‘s collection view to use your new layout.

Open Main.storyboard, click on BooksViewController then click on the Collection View. In the Attributes Inspector, set Layout to Custom and Class to BooksLayout as shown below:

VN_BooksLayoutStoryboard

Open BooksLayout.swift and add the following code above the BooksLayout class declaration.

private let PageWidth: CGFloat = 362
private let PageHeight: CGFloat = 568

These two constants will be used to set the size of the cell.

Now add the following initialization method within the class curly braces:

required init(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
 
  scrollDirection = UICollectionViewScrollDirection.Horizontal //1
  itemSize = CGSizeMake(PageWidth, PageHeight) //2
  minimumInteritemSpacing = 10 //3
}

Here’s what the code above does:

  1. Sets the collection view’s scroll view direction to horizontal.
  2. Sets the size of the cell to the page width of 362 and to a height of 568.
  3. Set the minimum spacing between cells to 10.

Next, add the following code after init(coder:):

override func prepareLayout() {
  super.prepareLayout()
 
  //The rate at which we scroll the collection view.
  //1
  collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
 
  //2
  collectionView?.contentInset = UIEdgeInsets(
    top: 0,
    left: collectionView!.bounds.width / 2 - PageWidth / 2,
    bottom: 0,
    right: collectionView!.bounds.width / 2 - PageWidth / 2
  )
}

prepareLayout() gives you the chance to perform any calculations before you come up with any layout information for each cell.

Taking each numbered comment in turn:

  1. Sets how fast the collection view will stop scrolling after a user lifts their finger. By setting it to UIScrollViewDecelerationRateFast the scroll view will decelerate much faster. Try playing around with Normal vs Fast to see the difference!
  2. Sets the content inset of the collection view so that the first book cover will always be centered.

Now you need to handle the layout information of each cell.

Add the following code below prepareLayout():

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
  //1
  var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
 
  //2
  for attributes in array {
    //3
    var frame = attributes.frame
    //4
    var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)
    //5
    var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)
    //6
    attributes.transform = CGAffineTransformMakeScale(scale, scale)
  }
 
  return array
}

layoutAttributesForElementsInRect(_:) returns an array of UICollectionViewLayoutAttributes objects, which provides the layout attributes for each cell. Here’s a breakdown of the code:

  1. Calling the superclass of layoutAttributesForElementsInRect returns an array that contains all default layout attributes for each cell.
  2. Loop through each attribute in the array.
  3. Grab the frame for the current cell attribute.
  4. Calculate the distance between the book cover — that is, the cell — and the center of the screen.
  5. Scale the book cover between a factor of 0.75 and 1 depending on the distance calculated above. You then scale all book covers by 0.7 to keep them nice and small.
  6. Finally, apply the scale to the book cover.

Next, add the following code right after layoutAttributesForElementsInRect(_:):

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
  return true
}

Returning true forces the layout to recalculate its attributes every time the collection view’s bound changes. A UICollectionView changes its bounds while scrolling, which is perfect for recalculating the cell’s attribute.

Build and run your app; you’ll see that book in the middle of the view is larger than the others:

VN_NotSnappy

Scroll through the books to see how each book cover scales up and down. But wouldn’t it be great if the book could snap into place, indicating the selection?

The next method you’ll add will do just that!

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:

  1. Create a new CGPoint called newOffset.
  2. Grab the current layout of the collection view.
  3. Get the total width of a cell.
  4. Calculate the current offset with respect to the center of the screen.
  5. If velocity.x > 0, the user is scrolling to the right. Think of offset/width as the book index you’d like to scroll to.
  6. If velocity.x = 0, the user didn’t put enough oomph into scrolling, and the same book remains selected.
  7. If velocity.x < 0, the user is scrolling left.
  8. 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:

VN_PageFlipping

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:

VN_BookLayoutStoryboard

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:

  1. Set the deceleration rate to UIScrollViewDecelerationRateFast to increase the rate at which the scroll view slows down.
  2. Grab the number of pages in the current book.
  3. 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:

  1. Create a new array to hold UICollectionViewLayoutAttributes.
  2. Loop through all the items (pages) in the collection view.
  3. For each item in the collection view, create an NSIndexPath.
  4. Grab the attribute for the current indexPath. You'll override layoutAttributesForItemAtIndexPath(_:) soon.
  5. Add the attributes to your array.
  6. Return all the cell attributes.

Handling the Page Geometry

Before you jump straight into the implementation of layoutAttributesForItemAtIndexPath(_:), take a minute to consider the layout, how it will work, and if you can write any helper methods to keep everything nice and modular. :]

VN_PaperRatioDiagram

The diagram above shows that every page flips with the book's spine as the axis of rotation. The ratios on the diagram range from -1.0 to 1.0. Why? Well, imagine a book laid out on a table, with the spine representing 0.0. When you turn a page from the left to the right, the "flipped" ratio goes from -1.0 (full left) to 1.0 (full right).

Therefore, you can represent your page flipping with the following ratios:

  • 0.0 means a page is at a 90 degree angle, perpendicular to the table.
  • +/- 0.5 means a page is at a 45 degree angle to the table.
  • +/- 1.0 means a page is parallel to the table.

Note that since angle rotation is counterclockwise, the sign of the angle will be the opposite of the sign of the ratio.

First, add the following helper method after layoutAttributesForElementsInRect(_:):

//MARK: - Attribute Logic Helpers
 
func getFrame(collectionView: UICollectionView) -> CGRect {
  var frame = CGRect()
 
  frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.x
  frame.origin.y = (collectionViewContentSize().height - PageHeight) / 2
  frame.size.width = PageWidth
  frame.size.height = PageHeight
 
  return frame
}

For every page, you calculate the frame with respect to the middle of the collection view. getFrame(_:) will align every page's edge to the book's spine. The only variable that changes is the collection view's content offset in the x direction.

Next, add the following method after getFrame(_:):

func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {
  //1
  let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5
 
  //2
  var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)
 
  //3
  if ratio > 0.5 {
    ratio = 0.5 + 0.1 * (ratio - 0.5)
 
  } else if ratio < -0.5 {
    ratio = -0.5 + 0.1 * (ratio + 0.5)
  }
 
  return ratio
}

The method above calculates the page's ratio. Taking each commented section in turn:

  1. Calculate the page number of a page in the book — keeping in mind that pages in the book are double-sided. Multiplying by 0.5 gives you the exact page you're on.
  2. Calculate the ratio based on the weighted percentage of the page you're turning.
  3. You need to restrict the page to a ratio between the range of -0.5 and 0.5. Multiplying by 0.1 creates a gap between each page to make it look like they overlap.

Once you've calculated the ratio, you'll use it to calculate the angle of the turning page.

Add the following code after getRatio(_:indexPath:):

func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {
  // Set rotation
  var angle: CGFloat = 0
 
  //1
  if indexPath.item % 2 == 0 {
    // The book's spine is on the left of the page
    angle = (1-ratio) * CGFloat(-M_PI_2)
  } else {
    //2
    // The book's spine is on the right of the page
    angle = (1 + ratio) * CGFloat(M_PI_2)
  }
  //3
  // Make sure the odd and even page don't have the exact same angle
  angle += CGFloat(indexPath.row % 2) / 1000
  //4
  return angle
}

There's a bit of math going on, but it's not so bad when you break it down:

  1. Check to see if the current page is even. This means that the page is to the right of the book's spine. A page turn to the right is counterclockwise, and pages on the right of the spine have a negative angle. Recall that the ratio you defined is between -0.5 and 0.5.
  2. If the current page is odd, the page is to the left of the book's spine. A page turn to the left is clockwise, and pages on the left side of the spine have a positive angle.
  3. Add a small angle to each page to give the pages some separation.
  4. Return the angle for rotation.

Once you have the angle, you need to transform each page. Add the following method:

func makePerspectiveTransform() -> CATransform3D {
  var transform = CATransform3DIdentity
  transform.m34 = 1.0 / -2000
  return transform
}

Modifying the m34 of the transform matrix adds a bit of perspective to each page.

Now it's time to apply the rotation. Add the following code:

func getRotation(indexPath: NSIndexPath, ratio: CGFloat) -> CATransform3D {
  var transform = makePerspectiveTransform()
  var angle = getAngle(indexPath, ratio: ratio)
  transform = CATransform3DRotate(transform, angle, 0, 1, 0)
  return transform
}

Here you use the two previous helper methods to calculate the transform and the angle, and create a CATransform3D to apply to the page along the y-axis.

Now that you have all the helper methods set up, you are finally ready to create the attributes for each cell. Add the following method after layoutAttributesForItemAtIndexPath(_:):

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
  //1
  var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
 
  //2
  var frame = getFrame(collectionView!)
  layoutAttributes.frame = frame
 
  //3
  var ratio = getRatio(collectionView!, indexPath: indexPath)
 
  //4
  if ratio > 0 && indexPath.item % 2 == 1
     || ratio < 0 && indexPath.item % 2 == 0 {
    // Make sure the cover is always visible
    if indexPath.row != 0 {
      return nil
    }
  }	
  //5
  var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))
  layoutAttributes.transform3D = rotation
 
  //6
  if indexPath.row == 0 {
    layoutAttributes.zIndex = Int.max
  }
 
  return layoutAttributes
}

You'll call this method for each cell in your collection view:

  1. Create a UICollectionViewLayoutAttributes object for the cell at the given NSIndexPath.
  2. Set the frame of the attribute using the getFrame method you created to ensure it's always aligned with the book's spine.
  3. Calculate the ratio of an item in the collection view using getRatio, which you wrote earlier.
  4. Check that the current page is within the ratio's threshold. If not, don't display the cell. For optimization purposes (and because of common sense), you won't display the back-side of a page, but only those that are front-facing — except when it's the book's cover, which you display at all times.
  5. Apply a rotation and transform with the given ratio you calculated.
  6. Check if indexPath is the first page. If so, make sure its zIndex is always on top of the other pages to avoid flickering effects.

Build and run your app, open up one of your books, flip through it and...whoa, what?
misc-jackie-chan

The pages seem to be anchored in their centers — not at the edge!

VN_Anchor1

As the diagram shows, each page's anchor point is set at 0.5 for both x and y. Can you tell what you need to do to fix this?

VN_CorrectRatio

It's clear you need to change the anchor point of a pages to its edge. If the page is on the right hand side of a book, the anchor point should be (0, 0.5). But if the page is on the left hand side of a book, the anchor point should be (1, 0.5).

Open BookPageCell.swift and add the following code:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
  super.applyLayoutAttributes(layoutAttributes)
  //1
  if layoutAttributes.indexPath.item % 2 == 0 {
    //2
    layer.anchorPoint = CGPointMake(0, 0.5)
    isRightPage = true
    } else { //3
      //4
      layer.anchorPoint = CGPointMake(1, 0.5)
      isRightPage = false
    }
    //5
    self.updateShadowLayer()
}

Here you override applyLayoutAttributes(_:), which applies the layout attributes created in BookLayout.

It's pretty straightforward code:

  1. Check to see if the current cell is even. This means that the book's spine is on the left of the page.
  2. Set the anchor point to the left side of the cell and set isRightPage to true. This variable helps you determine where the rounded corners of the pages should be.
  3. If the current cell is odd, then the book's spine is on the right side of the page.
  4. Set the anchor point to the right side of the cell and set isRightPage to false.
  5. Finally, update the shadow layer of the current page.

Build and run your app; flip through the pages and things should look a little better:

VN_CompletePart1

That's it for the first part of this tutorial! Take some time to bask in the glory of what you've created — it's a pretty cool effect! :]

Where to Go From Here?

You can download the completed project from Part 1 that contains all the source code.

You started out with the default layouts for a collection view, and learned to customize a new layout to turn it into something truly amazing! Someone using this app will feel like they are flipping through a real book. It's the little things that turn a normal reader app into something that people can feel truly connected with.

However, you're not done yet! You'll make this app even better and more intuitive in Part 2 of this tutorial, where you'll explore custom transitions between the closed and opened book views.

Do you have any crazy layout ideas you are considering for your own app? If you have any questions, comments or other ideas from this tutorial, please join the discussion below!

The post How to Create an iOS Book Open Animation: Part 1 appeared first on Ray Wenderlich.

How to Create an iOS Book Open Animation: Part 2

$
0
0

Learn how to create a cool book opening animation!

Welcome back to our iOS book open animation tutorial series!

In the first part of this tutorial series, you learned how to create two custom collection view layouts and applied shadow layers to the book’s pages to create depth and realism in your app.

In this final part, you’ll learn to create custom navigation transitions and apply interactive gestures to open a book with a pinch gesture.

Note: Full credit goes to Attila Hegedüs for creating this awesome sample project.

Getting Started

The tutorial picks up from Part 1. If you didn’t work through the last part, or want to start afresh, simply download the completed sample project from the previous tutorial.

VN2_Start

Open up the project in Xcode. Right now, when you select a book to read the open pages simply slide in from the right. This is the default transition behavior for a UINavigationController. But by the end of this tutorial, your custom transition will look like the following:

VN_BookOpening

The custom transition will animate the book smoothly between the closed and opened states in a natural manner that users will love.

Time to get started!

Creating your Custom Navigation Controller

To create a custom transition on a push or pop you must create a custom navigation controller and implement the UINavigationControllerDelegate protocol.

Right-click (or Ctrl-click) on the App group and click New File. Select the iOS\Source\Cocoa Touch Class template and name the new file CustomNavigationController. Make sure it’s a subclass of UINavigationController and set the language to Swift. Click Next and then Create.

Open CustomNavigationController.swift and replace its contents with the following:

import UIKit
 
class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
 
  override func viewDidLoad() {
    super.viewDidLoad()
    //1
    delegate = self
  }
 
  //2
  func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if operation == .Push {
      return nil
    }
 
    if operation == .Pop {
      return nil
    }
 
    return nil
  }
}

Here’s what you’re doing in the code above:

  1. In viewDidLoad you set the navigation controller as its own delegate.
  2. navigationController(_:animationControllerForOperation:fromViewController:toViewController:) is one of the methods you can implement for UINavigationControllerDelegate. This method executes each time you push or pop between view controllers, and you control which animated transition you return from this method. The code currently returns nil which defaults to the standard transition. You’ll replace it with your own custom transition object shortly.

Now that you have your custom navigation controller set up, it’s time to replace the default navigation controller in storyboard.

Open Main.storyboard and click Navigation Controller in the storyboard’s view hierarchy on the left. Next, click the Identity Inspector and under Custom Class, change UINavigationController to CustomNavigationController, as shown below:

VN_storyboard2

Build and run to ensure everything still works; nothing will have changed since you’re returning nil in your delegate method, which defaults to the navigation controller’s standard transition.

Creating the Custom Transition

Time for the fun part — building your custom transition object! :]

With a custom transition object, the class you create must conform to the UIViewControllerAnimatedTransitioning protocol, and in particular, the methods below:

  • transitionDuration: Required. Returns the duration of the animation and synchronizes interactive transitions.
  • animateTransition: Required. Provides the to and from controllers you’re transitioning between. Most of the heavy lifting will be done in this method.
  • animationEnded: Optional. Informs you when the transition has finished. You can perform any required cleanup in this method.

Setting up Your Transition

Right-click (or Ctrl-click) on the App group and click New File. Select the iOS\Source\Cocoa Touch Class template and name the new file BookOpeningTransition. Make sure it’s a subclass of NSObject and set the language to Swift. Click Next and then Create.

Open BookOpeningTransition.swift and replace its contents with the following:

import UIKit
 
//1
class BookOpeningTransition: NSObject, UIViewControllerAnimatedTransitioning {
 
  // MARK: Stored properties
  var transforms = [UICollectionViewCell: CATransform3D]() //2
  var toViewBackgroundColor: UIColor? //3
  var isPush = true //4
 
  //5
  // MARK: UIViewControllerAnimatedTransitioning
  func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 1
  }
 
  func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
 
  }
}

Taking each commented section in turn:

  1. BookOpeningTransition implements the required methods for the UIViewControllerAnimatedTransitioning protocol.
  2. The dictionary transforms stores key value pairs, where the key is a UICollectionViewCell and the value is of type CATransform3D. This dictionary tracks each cell’s page transform when the book is open.
  3. This defines the color you transition to, which helps the fade look much cleaner.
  4. The boolean isPush determines whether the transition is a push, or a pop,
  5. Here you add the required methods for UIViewControllerAnimatedTransitioning to avoid build errors; you’ll implement these methods shortly.

Now that you have your variables set up, it’s time to implement the protocol methods.

Replace the contents of transitionDuration(_:) with the following:

if isPush {
  return 1
} else {
  return 1
}

transitionDuration(_:) returns the duration of the transition animation. In this case, you want it to take 1 second on either a push or a pop. Writing the method this way lets you easily change the timing of the push or pop.

Next, you need to implement the second required protocol method — animateTransition — where the magic will happen! :] You’ll implement this in two parts:

  1. Implement the helper methods to set up animateTransition for a push.
  2. Implement the helper methods to set up animateTransition for a pop.

Creating the Push Transition

Imagine yourself opening a book in real life:

VN_PushStage

Although it looks complicated, you only need to consider the two states of your animation and let UIView‘s method animateWithDuration handle the animation between the following two states:

  1. Stage 1 is when the book is closed.
  2. Stage 2 is when the book is open; this is essentially the transform you created in Part 1 of this tutorial.

First, you’ll implement some helper methods to handle the two states before you implement the animateTransition(_:) protocol method.

Still in BookOpeningTransition.swift, add the following code to the end of the class:

// MARK: Helper Methods
func makePerspectiveTransform() -> CATransform3D {
  var transform = CATransform3DIdentity
  transform.m34 = 1.0 / -2000
  return transform
}

This code returns a transform and adds perspective in the z-axis. You’ll use this later to help transform your views during the animation.

State 1 – Closed Book

Next, add the following code just after makePerspectiveTransform:

func closePageCell(cell : BookPageCell) {
  // 1
  var transform = self.makePerspectiveTransform()
  // 2
  if cell.layer.anchorPoint.x == 0 {
    // 3
    transform = CATransform3DRotate(transform, CGFloat(0), 0, 1, 0)
    // 4
    transform = CATransform3DTranslate(transform, -0.7 * cell.layer.bounds.width / 2, 0, 0)
    // 5
    transform = CATransform3DScale(transform, 0.7, 0.7, 1)
   }
   // 6
   else {
     // 7
     transform = CATransform3DRotate(transform, CGFloat(-M_PI), 0, 1, 0)
     // 8
     transform = CATransform3DTranslate(transform, 0.7 * cell.layer.bounds.width / 2, 0, 0)
     // 9
     transform = CATransform3DScale(transform, 0.7, 0.7, 1)
    }
 
    //10
    cell.layer.transform = transform
}

Recall that the BookViewController is a collection view of pages. You transformed every page to align to the book’s spine, and rotated it on an axis to achieve the page flipping effect. Initially, you want the book to be closed. This method transitions every cell (or page) to be flat and fit behind the book’s cover.

Here’s a quick illustration of the transform:

VN2_ClosedState

Here’s an explanation of the code that makes that happen:

  1. Initialize a new transform using the helper method you created earlier.
  2. Check that the cell is a right-hand page.
  3. If it’s a right-hand page, set its angle to 0 to make it flat.
  4. Shift the page be centered behind the cover.
  5. Scale the page on the x and y axes by 0.7. Recall that you scaled the book covers to 0.7 in the previous tutorial, in case you wondered where this magic number came from.
  6. If the cell isn’t a right-hand page, then it must be a left-hand page.
  7. Set the left-hand page’s angle to 180. Since you want the page to be flat, you need to flip it over to the right side of the spine.
  8. Shift the page to be centered behind the cover.
  9. Scale the pages back to 0.7.
  10. Finally, set the cell’s transform.

Now add the following method below the one you added above:

func setStartPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
  // 1
  toViewBackgroundColor = fromVC.collectionView?.backgroundColor
  toVC.collectionView?.backgroundColor = nil
 
  //2
  fromVC.selectedCell()?.alpha = 0
 
  //3
  for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
    //4
    transforms[cell] = cell.layer.transform
    //5
    closePageCell(cell)
    cell.updateShadowLayer()
    //6
    if let indexPath = toVC.collectionView?.indexPathForCell(cell) {
      if indexPath.row == 0 {
        cell.shadowLayer.opacity = 0
      }
    }
  }
}

setStartPositionForPush(_:toVC:) sets up stage 1 of the transition. It takes in two view controllers to animate:

  • fromVC, of type BooksViewController, lets you scroll through your list of books.
  • toVC, of type BookViewController, lets you flip through the pages of the book you selected.

Here’s what’s going on in the code above:

  1. Store the background color of BooksViewController‘s collection view and set BookViewController‘s collection view background to nil.
  2. Hide the selected book cover. toVC will now handle the display of the cover image.
  3. Loop through the pages of the book.
  4. Save the current transform of each page in its opened state.
  5. Since the book starts from a closed state, you transform the pages to closed and update the shadow layer.
  6. Finally, ignore the shadow of the cover image.

State 2 – Opened Book

Now that you’ve finished state 1 of the transitions, you can move on to state 2, where you go from a closed book to an opened book.

Add the following method below setStartPositionForPush(_:toVC:)):

func setEndPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
  //1
  for cell in fromVC.collectionView!.visibleCells() as! [BookCoverCell] {
    cell.alpha = 0
  }
 
  //2
  for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
    cell.layer.transform = transforms[cell]!
    cell.updateShadowLayer(animated: true)
  }
}

Digging into the code above:

  1. Hide all the book covers, since you’re presenting the selected book’s pages.
  2. Go through the pages of the selected book in BookViewController and load the previously saved open transforms.

After you push from BooksViewController to BookViewController, there’s a bit of cleanup to do.

Add the following method just after the one you added above:

func cleanupPush(fromVC: BooksViewController, toVC: BookViewController) {
  // Add background back to pushed view controller
  toVC.collectionView?.backgroundColor = toViewBackgroundColor
}

Once the push is complete, you simply set the background color of BookViewController‘s collection view to the background color you saved earlier, hiding everything behind it.

Implementing the Book Opening Transition

Now that you have your helper methods in place, you’re ready to implement the push animation! Add the following code to the empty implementation of animateTransition(_:):

//1
let container = transitionContext.containerView()
//2
if isPush {
  //3
  let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BooksViewController
  let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BookViewController
  //4
  container.addSubview(toVC.view)
 
  // Perform transition
  //5
  self.setStartPositionForPush(fromVC, toVC: toVC)
 
  UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: nil, animations: {
    //6
    self.setEndPositionForPush(fromVC, toVC: toVC)
    }, completion: { finished in
      //7
      self.cleanupPush(fromVC, toVC: toVC)
      //8
      transitionContext.completeTransition(finished)
  })
} else {
  //POP
}

Here’s what’s happening in animateTransition(_:):

  1. Get the container view, which acts as the superview between the transitioning view controllers.
  2. Check that you’re performing a push.
  3. If so, get both fromVC (BooksViewController) and toVC (BookViewController).
  4. Add toVC (the BookViewController) to the containing view.
  5. Set up the starting positions for the to and from view controllers for the closed state.
  6. Next, you animate from the starting position (Closed State) to the ending position (Opened State)
  7. Perform any cleanup.
  8. Notify the system that the transition is complete.

Applying the Push Transition to the Navigation Controller

Now that you have your push transition set up, it’s time to apply it to your custom navigation controller.

Open BooksViewController.swift and add the following property just after the class declaration:

var transition: BookOpeningTransition?

This property keeps track of your transition, letting you know whether the transition is a push or pop.

Next add the following extension after the ending curly brace:

extension BooksViewController {
func animationControllerForPresentController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  // 1
  var transition = BookOpeningTransition()
  // 2
  transition.isPush = true
  // 3
  self.transition = transition
  // 4
  return transition
  }
}

This creates an extension to separate parts of the code’s logic. In this case, you want to group methods related to transitions in one place. This method sets up the transition object and returns it as well.

Taking a closer look at the code:

  1. Create a new transition.
  2. Since you are presenting the controller, or pushing, set isPush to true.
  3. Save the current transition.
  4. Return the transition.

Now open CustomNavigationController.swift and replace the push if statement with the following:

if operation == .Push {
  if let vc = fromVC as? BooksViewController {
    return vc.animationControllerForPresentController(toVC)
  }
}

This checks that the view controller you’re pushing from is a BooksViewController, and presents BookViewController with the transition you created: BookOpeningTransition.

Build and run your app; click on a book of your choice and you’ll see the book animate smoothly from closed to opened:

VN_PushGlitch

Uh..how come it’s not animating?

angry-desk-flip

It’s jumping straight from a closed book to an opened book because you haven’t loaded the pages’ cells!

The navigation controller transitions from BooksViewController to BookViewController, which are both UICollectionViewControllers. UICollectionView cells don’t load on the main thread, so your code sees zero cells at the start — and thinks there’s nothing to animate!

You need to give the collection view enough time to load all the cells.

Open BooksViewController.swift and replace openBook(_:) with the following:

func openBook(book: Book?) {
  let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
  vc.book = selectedCell()?.book
  //1
  vc.view.snapshotViewAfterScreenUpdates(true)
  //2
  dispatch_async(dispatch_get_main_queue(), { () -> Void in
    self.navigationController?.pushViewController(vc, animated: true)
    return
  })
}

Here’s how you solved the problem:

  1. You tell BookViewController to create a snapshot after the changes have been incorporated.
  2. Make sure you push BookViewController on the main thread to give the cells time to load.

Build and run your app again; you should see the book animate properly on a push:

VN_PushGlitchAnimate

That looks much better! :]

Now that you’re done with the push transition, you can move on to the pop transition.

Implementing the Pop Helper Methods

Popping the view controller is pretty much the opposite of a push. Stage 1 is now the open book state, and Stage 2 is now the closed book state:

VN_PopState

Open up BookOpeningTransition.swift and add the following code:

// MARK: Pop methods
func setStartPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
  // Remove background from the pushed view controller
  toViewBackgroundColor = fromVC.collectionView?.backgroundColor
  fromVC.collectionView?.backgroundColor = nil
}

setStartPositionForPop(_:toVC) only stores the background color of BookViewController and removes the background color of BooksViewController‘s collection view. Note that you don’t need to set up any cell transforms, since the book is currently in its opened state.

Next, add the following code for setEndPositionForPop(_:toVC) immediately after the code you just added above:

func setEndPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
  //1
  let coverCell = toVC.selectedCell()
  //2
  for cell in toVC.collectionView!.visibleCells() as! [BookCoverCell] {
    if cell != coverCell {
      cell.alpha = 1
    }
  }      
  //3
  for cell in fromVC.collectionView!.visibleCells() as! [BookPageCell] {
    closePageCell(cell)
  }
}

This method sets up the end state of the pop transition where the book goes from opened to closed:

  1. Get the selected book cover.
  2. In the closed book state, loop through all the book covers in BooksViewController and fade them all back in.
  3. Loop through all the pages of the current book in BookViewController and transform the cells to a closed state.

Now add the following method:

func cleanupPop(fromVC: BookViewController, toVC: BooksViewController) {
  // Add background back to pushed view controller
  fromVC.collectionView?.backgroundColor = self.toViewBackgroundColor
  // Unhide the original book cover
  toVC.selectedCell()?.alpha = 1
}

This method performs some cleanup once the pop transition has finished. The cleanup process sets BooksViewController‘s collection view background to its original state and displays the original book cover.

Now add the following code within the protocol method animateTransition(_:) inside the else block of the code with the //POP comment:

//1
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BookViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BooksViewController
 
//2
container.insertSubview(toVC.view, belowSubview: fromVC.view)
 
//3
setStartPositionForPop(fromVC, toVC: toVC)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
  //4
  self.setEndPositionForPop(fromVC, toVC: toVC)
}, completion: { finished in
  //5
  self.cleanupPop(fromVC, toVC: toVC)
  //6
  transitionContext.completeTransition(finished)
})

Here’s how the pop transition animation works:

  1. Grab the view controllers involved in the transition. fromVC is now BookViewController (the opened book state) and toVC is now the BooksViewController (closed book state).
  2. Add BooksViewController below BookViewController within the container view.
  3. setStartPositionForPop(_:toVC) stores the background color before setting it to nil.
  4. Animate from the opened book state to the closed book state.
  5. Clean up the view controller once the animation is done by setting the background color back to it’s original color and showing the book cover.
  6. Notify the transition is complete.

Applying the Pop Transition to the Navigation Controller

Now you need to set up the pop transition just as you did with the push transition.

Open BooksViewController.swift and add the following method right after animationControllerForPresentController(_:):

func animationControllerForDismissController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  var transition = BookOpeningTransition()
  transition.isPush = false
  self.transition = transition
  return transition
}

This again creates a new BookOpeningTransition, but the only difference is that the transition is now set to be a pop.

Now open CustomNavigationController.swift and replace the pop if statement with the following:

if operation == .Pop {
  if let vc = toVC as? BooksViewController {
    return vc.animationControllerForDismissController(vc)
  }
}

This returns the transition and performs the pop animation to close the book.

Build and run your app; select a book to see it open and close as shown below:

VN_OpenAndClose

Creating an Interactive Navigation Controller

The opening and closing transition animations look great — but you can still do better! You can apply intuitive gestures to pinch the book open or closed.

First, open BookOpeningTransition.swift and add the following property:

// MARK: Interaction Controller
var interactionController: UIPercentDrivenInteractiveTransition?

Next open CustomNavigationController.swift and add the following code:

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
  if let animationController = animationController as? BookOpeningTransition {
    return animationController.interactionController
  }
  return nil
}

In the above method, you return the interactive animator object from BookOpeningTransition. This lets the navigation controller keep track of the progress of the animation so the user can interactively pinch a book opened or closed.

Now open BooksViewController.swift and add the following property under the transition variable:

//1
var interactionController: UIPercentDrivenInteractiveTransition?
//2
var recognizer: UIGestureRecognizer? {
  didSet {
    if let recognizer = recognizer {
      collectionView?.addGestureRecognizer(recognizer)
    }
  }
}

Here’s why you added these variables:

  1. interactionController is of class type UIPercentDrivenInteractiveTransition, which manages the custom animation between the view controllers transitioning in and out. The interaction controller also depends on a transition animator, which is a custom object that implements the UIViewControllerAnimatorTransitioning protocol. You’ve created BookOpeningTransition — which does exactly that!

    The iteractionController can control the progress between pushing and popping view controllers. To learn more about this class, read up on Apple’s documentation on how this works.

  2. recognizer is a UIGestureRecognizer. You’ll use the gesture recognizer to pinch the book in and out.

Now add the following snippet under the transition.isPush = true line of your BooksViewController extension in animationControllerForPresentController(_:):

transition.interactionController = interactionController

This lets your custom navigation controller know which interaction controller to use.

Add the same code to animationControllerForDismissController(_:), under transition.isPush = false:

transition.interactionController = interactionController

Next, add the following code to viewDidLoad():

recognizer = UIPinchGestureRecognizer(target: self, action: "handlePinch:")

This initializes a UIPinchGestureRecognizer, which lets the user perform a pinch gesture with the action method handlePinch(_:).

Implement the action under viewDidLoad() like so:

// MARK: Gesture recognizer action
func handlePinch(recognizer: UIPinchGestureRecognizer) {
  switch recognizer.state {
    case .Began:
      //1
      interactionController = UIPercentDrivenInteractiveTransition()
      //2
      if recognizer.scale >= 1 {
        //3
        if recognizer.view == collectionView {
          //4
          var book = self.selectedCell()?.book
          //5
          self.openBook(book)
        }
      //6
      } else {
        //7
        navigationController?.popViewControllerAnimated(true)
      }        
    case .Changed:
      //8
      if transition!.isPush {
        //9
        var progress = min(max(abs((recognizer.scale - 1)) / 5, 0), 1)
        //10
	interactionController?.updateInteractiveTransition(progress)
	//11
      } else {
        //12
	var progress = min(max(abs((1 - recognizer.scale)), 0), 1)
        //13
	interactionController?.updateInteractiveTransition(progress)
      } 
    case .Ended:
      //14
      interactionController?.finishInteractiveTransition()
      //15
      interactionController = nil
    default:
      break
  }
}

For the UIPinchGestureRecognizer, you’ll keep track of three different states. The state began lets you know when the pinch has started. The state changed detects changes to the pinch, and ended lets you know when the pinch has ended.

The rundown of your implementation of handlePinch(_:) code is below:

Began State

  1. Instantiate a UIPercentDrivenInteractiveTransition object.
  2. Check that the scale, which is dependent on the distance between the pinch points, is greater than or equal to 1.
  3. If so, ensure that the view you involved in the gesture is indeed a collection view.
  4. Grab the book being pinched.
  5. Perform a push of BookViewController to show the pages of the book.
  6. If the scale is less than 1
  7. …perform a pop of BookViewController to show the book cover again.

Changed State – While Pinching

  1. Check to see if the current transition is performing a push.
  2. If you’re pushing to BookViewController, obtain the progress of the user’s pinch gesture. progress must be between 0 and 1. You scale the pinch down to one-fifth of its original value; this gives the user more control over the transition. Otherwise pinching a book open would appear to jump to the opened state immediately.
  3. Update the completed percentage of the transition based on the progress you calculated earlier.
  4. If the current transition is not performing a push, then it must be performing a pop.
  5. While pinching the book closed, the scale must progress from 1 to 0.
  6. Finally, update the progress of the transition.

End State – Stop Pinching

  1. Notify the system that the user interaction of the transition is complete.
  2. Set the interaction controller to nil.

Finally, you need to implement the pinch-to-closed state. Therefore you have to pass the gesture recognizer to BookViewController so it can pop itself.

Open up BookViewController.swift, and add the following property under the book variable:

var recognizer: UIGestureRecognizer? {
  didSet {
    if let recognizer = recognizer {
      collectionView?.addGestureRecognizer(recognizer)
    }
  }
}

Whenever you set the gesture recognizer in BookViewController, the gesture will be added immediately to the collection view so you can track the pinch gesture as the user closes the book.

Next you need to pass the gesture recognizer between the BooksViewController and BookViewController.

Open up BookOpeningTransition.swift. Add the following lines to cleanUpPush(_:toVC) after the point where you set the background color:

// Pass the gesture recognizer
toVC.recognizer = fromVC.recognizer

Once you’ve pushed from BooksViewController to the BookViewController, you pass the pinch gesture to BookViewController. This automatically adds the pinch gesture to the collection view.

When you pop from BookViewController to BooksViewController, you have to pass the pinch gesture back.

Add the following line to cleanUpPop(_:toVC), just after the line where you set the background color:

// Pass the gesture recognizer
toVC.recognizer = fromVC.recognizer

Build and run your app; select any book and use a pinch gesture to open and close the book:

VN_Pinching

The pinch gesture is a natural mechanism to open and close a book; it is also an opportunity to clean up your interface. You don’t need that Back button in the navigation bar anymore — time to get rid of it.

Open Main.storyboard, select Custom Navigation View Controller, open the Attributes Inspector and uncheck Bar Visibility under the Navigation Controller section, like so:

Screen Shot 2015-03-14 at 1.16.36 PM

Build and run your app again:

VN_BookOpening

Much cleaner! :]

Where To Go From Here?

You can download the final project from this part with all the code from the tutorial above.

In this tutorial, you learned how to apply custom layouts to collection views to give the user a more natural, and decidedly more interesting, experience with your app. You also created custom transitions and applied smart interactions to pinch a book opened and closed. Your app, while still solving the same basic problem, gives the application much more personality and helps it stand out from the rest.

Is it easier to go with the default “ease-in/ease-out” animations? Well, you could save a little development time. But the best apps have that extra bit of customized polish that makes them shine. Everyone remembers the apps they downloaded that were a lot of fun to use; you know, the ones that gave you a little UI thrill, but without sacrificing utility.

I hope you enjoyed this tutorial, and again I would like to thank Attila Hegedüs for creating this awesome project.

If you have any question about this tutorial, please join the forum discussion below!

The post How to Create an iOS Book Open Animation: Part 2 appeared first on Ray Wenderlich.

RWDevCon 2016: Tickets Now Available!

$
0
0

RWDevCon 2016: Tickets Now Available!

Next March, we are running an iOS conference focused on high quality hands-on tutorials called RWDevCon 2016.

Today, the team and I are happy to announce that RWDevCon 2016 tickets are now available!

And good news – for a limited time, we are offering an early bird discount where you can get a $100 discount off the standard ticket price.

Keep reading to find out what makes RWDevCon special, and what’s in store this year!

What Makes RWDevCon Special

RWDevCon is designed around 4 main principles:

1) Hands-On Tutorials

RWDevCon is the first ever conference focused on high quality hands-on tutorials. It has three tracks of tutorials going on simultaneously, leading to some challenging and fun choices on which to attend! :]

Screen Shot 2014-10-26 at 5.39.55 PMIn a typical tutorial, you’ll come in and the instructor will give a brief overview of the topic, and then you’ll get right into a live demo. But instead of just watching the instructor, you’ll follow along with the instructor so you can see things working for yourself.

After the demo, you’ll break out into a hands-on lab, where you’ll be challenged to try things out on your own – and the instructor will be right with you if you have any questions.

We really think this hands-on experience is the best way to learn, and this way you won’t just leave with notes and references – you’ll leave with actual new skills.

“The hands-on tutorials were worth their weight in gold. Some of the concepts I spend hours reading about were cleared up in 30 minutes.” –RWDevCon 2015 attendee

2) Team Coordination

Just like we do for books and tutorials on this site, RWDevCon is highly coordinated as a team. This lets us:

  • Cover the material you want. We send a survey to all attendees with potential tutorial topics, and let everyone vote on what they want. We then cover the top voted topics.
  • Practice and polish. Each speaker is assigned a partner, and practices once with their partner, and once with me. The result is a well polished talks that give you a great experience.
  • Coordinate materials. We develop our tutorial materials in a standard format, and the result is high quality content that remains useful even after the conference – check out last year’s!
“The demos and labs have been SO polished. Basically my ideal iOS conference because I came away learning so much!” –RWDevCon 2015 attendee

3) Inspiration

Contributing-Thumb

After a long days work on hands-on tutorials, you’ll be ready for a break.

That’s why in the afternoon, we switch to something completely different: inspiration talks.

These are short 18-minute non-technical talks with the goal of giving you a new idea, some battle-won advice, and leaving you excited and energized.

If you’re curious what these look like, check out our RWDevCon 2015 videos.

“The inspiration talks surprised me the most. I really enjoyed those. I loved all of the technical sessions but the inspiration talks were unexpected.” –RWDevCon 2015 attendee

4) Friendship

We believe one of the best parts about going to a conference is the people – so get ready to party and play!

01_PennSocial

We’ll have an opening reception before the conference begins to get to meet each other, and an awesome party on Friday night. We have some surprises up our sleeves this year too – you won’t want to miss it! :]

“Better food and more fun than WWDC! Lots left to work on after I go home – really met the goal of providing content to cement the knowledge.” –RWDevCon 2015 attendee

Where To Go From Here?

If you’re interested in getting a ticket, now’s the best time:

  • Before it sells out: RWDevCon only has a limited amount of spaces, and last year the conference sold out very quickly, so now’s your chance to have a guaranteed ticket.
  • To lock in your discount: Currently we have an Early Bird discount available that gives you a $100 discount off the standard ticket price – but this won’t last long!

You can grab your ticket at the RWDevCon web site. We hope to see you there! :]

The post RWDevCon 2016: Tickets Now Available! appeared first on Ray Wenderlich.

How To Change Your App Icon at Build Time

$
0
0
Beta ribbon and build number.

Beta ribbon and build number.

So, you’ve finished a beta version of your app, and finally there are some fresh eyeballs and brains helping you test and perfect the app that’s going to make you rich…or at least fatten your portfolio.

But wouldn’t it be helpful if testers had an easy way to check which build version of the app they have?

This tutorial will show you how to do that, and perhaps introduce you to a few lesser-known features of Xcode.

Would you believe you aren’t going to write a single line of Swift in this tutorial? No, seriously, and you won’t be writing any Objective-C either. :]

This tutorial will have you writing only bash shell scripts. You’ll use a tool called ImageMagick, Terminal and Xcode to write a script that will automatically overlay the build version and a “debug” or “beta” ribbon to your app’s icon.

This tutorial assumes you have some basic knowledge of Unix scripting. You can probably follow along without being a shell scripting guru, but you can also look up what you need at Bash Beginners Guide or Advanced Bash Scripting Guide .

So, do you want get going?

Getting Started

First things first, you’ll need ImageMagick installed, which is a powerful image processing software suite that you interact with exclusively via Terminal.

You can easily install ImageMagick with Homebrew.

If you don’t have Homebrew installed or perhaps don’t know what it’s, you can learn about it and how to install from the tools homepage.

If you already have Homebrew installed, it’s still a good idea to launch Terminal and enter

brew update

This will make sure that you’re getting the latest version of any package you install from Homebrew. It will also inform you if you don’t have Homebrew installed.

OK cool, you’re all set to use Homebrew to install the packages you need. Now run the following command:

brew install ImageMagick

monologue

You should see a monologue from Homebrew that explains the installation of ImageMagick, so follow the steps to install the program.

You’ll also need to install the software suite Ghostscript, because the text functions you’ll need in ImageMagick depend on it. Ghostscript is a software suite designed for rendering PDF and PS files. You need it because it provides the font support for ImageMagick.

Install Ghostscript by running the command:

brew install ghostscript

If you encounter any difficulties later on, run this command:

brew doctor

If anything has gone awry, you’ll get a message about it, along with instructions for how to fix it.

And that’s all you need to install to be able to work through this tutorial.

Hello Llama

ImageMagick has many commands, but the two you need for this tutorial are convert and composite.

  • convert takes an image, modifies it, and then saves the result as a new image.
  • composite takes an image, overlays it on another image, and outputs the result to a third image.

This tutorial provides some sample icons for you to use. Of course, you may use your own, but you’ll need to adapt the file names accordingly. Download the sample icons, and for the purposes of this tutorial, place them on the desktop.

One of the goals here is to overlay the build version on the app’s icon. So you’ll see how to use ImageMagick to overlay text on an image by putting Hello World on one of these icons. Open Terminal and navigate to the app icon folder:

cd ~/Desktop/AppIconSet

Now type:

convert AppIcon60x60@2x.png -fill white -font Times-Bold -pointsize 18 -gravity south -annotate 0 "Hello World" test.png

I’ll break this command down for you, parameter by parameter, so you understand what’s going on:

  • AppIcon60x60@2x.png is the input image’s file name;
  • fill white sets the text’s fill color to white;
  • font Times-Bold instructs ImageMagick to use the bold variant of the Times typeface for any text;
  • pointsize 18 sets the font size to 18 pixels;
  • gravity south means any text generated by the command will be aligned at the bottom of the image
  • annotate 0 “Hello World” tells ImageMagick to annotate the image with the text Hello World at an angle of 0 degrees;
  • test.png is the name of the output file, and ImageMagick will overwrite the existing file if it already exists.

If you didn’t see any errors on the screen, you will now see a file named test.png in the AppIconSet folder, and it will look like this:

test

Note: If you get error messages or the script simply doesn’t work, it’s possible that you don’t have the necessary fonts installed. To find out which fonts are available to you, run the following command in Terminal:

convert -list font

If you don’t have the Times font, choose one from the list and use that instead.

OK, now that you’ve done that, you’ll put a beta ribbon on it. In Terminal type:

composite betaRibbon.png test.png test2.png

This takes betaRibbon.png and places it on top of test.png, and then saves the composited image to test2.png.

Open test2.png. Wait, It looks just like the original test.png!

test2

So here’s what’s going on:

test.png is 120 x 120 pixels in size. However, betaRibbon.png is 1024×1014 pixels, so only the transparent parts of betaRibbon.png are applied to test.png, and the rest is cropped.

Don’t believe me? Try running the same command, but swap the places of betaRibbon.png and test.png:

composite test.png betaRibbon.png test2.png

You’ll now see a much larger image with the ribbon where it’s always been and the test.png image aligned at the top left:

test2

For this to work correctly, you need to resize betaRibbon.png to be 120 x 120. This is very easy in ImageMagick – just type:

convert betaRibbon.png -resize 120x120 smallBetaRibbon.png

This command resizes betaRibbon.png to 120 x 120 pixels and saves the result as smallBetaRibbon.png.

Now, execute the following:

composite smallBetaRibbon.png test.png test2.png

Open test2.png, and you’ll see it now looks like it’s supposed to:

test2

So that’s all of the ImageMagick functionality you’ll need for this tutorial, but know that it’s just the tip of the iceberg as far as what ImageMagick can do. Check out ImageMagick’s home page to learn more.

Xcode

After all that image processing work, it’s time to cleanse your palate by working in some familiar territory.

Open Xcode, select File\New\Project…, choose the iOS\Application\Single View Application template, and click Next. Name the project Llama Trot, set the selected language to Swift, and set the device family to Universal. For simplicity, save the project to the desktop.

Your goal is to have Xcode and ImageMagick generate an appropriate icon every time you build the project, based on the selected build configuration.

Xcode has the ability to use run scripts to do things when your project is built. A run script is just a Unix script, like what you’ve already written, that executes every time you run your Xcode project.

Setting Up a Run Script

In your project, select the Llama Trot target, and navigate to Build Phases. Click the + icon and select New Run Script Phase from the popup menu:

Screen Shot 2015-05-10 at 10.53.12 PM

You’ll see the Run Script Phase added to your project’s phases.

Within that run script, the Shell parameter should be auto-set to bin/sh, which means that this script will execute in the bash Unix shell.

Underneath the shell parameter is a box for you to enter your script. Type the line below into that box:

echo "Hello World"

Your new build phase should now look like the following:

Screen Shot 2015-05-10 at 10.54.54 PM

Build and run. You should see absolutely nothing interesting. That’s because the script printed Hello World to your build logs while you weren’t looking.

Navigate to the Report Navigator-that’s the icon on the far right of Xcode’s Navigator Pane-and click on the most recent build report, as shown in the image below. In the wall of text describing all of the work that Xcode does for you when you build a project, you’ll see the text Hello World:

HelloWorld

App Icons

Great, you’ve gotten a script to output Hello World, like you’ve probably done a million times by this point in your career as a developer. Now it’s time to modify the app icons.

Finding the App Icons From Your Script

Add all the app icons to your Images.xcassets asset catalog by navigating to Images.xcassets in Xcode and dragging each icon into it’s appropriate tile in the AppIcon image set:

AppIcon

You’ll also need to put debugRibbon.png and betaRibbon.png in your project’s root directory-the same folder where your project’s .xcodeproj file is.

Screen Shot 2015-05-10 at 10.47.33 PM

In order do anything with the icons, your script needs to know where they are. Replace your run script with the following code:

echo "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
echo "${SRCROOT}"
  1. The first line prints the path to the folder that will contain the final icons after you run your project.
  2. The second line prints the folder path of where your project files are located.

This works by using some of Xcode’s many build settings’ variables.

Run your project and check the build report. You should see the folder path that describes the location of your project’s final products. Under that, you should see the folder path of your project directory:

Paths

Navigate to the first path in Finder and look at what’s in there; you’ll see all of the products of your app, including all of the app icons. This is where you’ll save the icons that ImageMagick modifies.

To see these icons, right-click on the Application icon in that folder, and then select Show Package Contents. For now they look perfectly normal!

Finder

Now navigate to the second folder path that you echoed out. This is just your normal project folder. So where are the app icons?

Go into the folder that has the same name as your project-in this case “Llama Trot”. In there you’ll see Images.xcassets. Open Images.xcasset and you’ll see yet another folder named AppIcon.appiconset.

The app icons are in that folder, and you’ll modify them with ImageMagick. Assuming you saved your project to the desktop and named it Llama Trot, the path to the icons is ~/Desktop/Llama\ Trot/Llama\ Trot/Images.xcassets/AppIcon.appiconset

You’re going to cheat a little bit to get the full path to the initial icons, so replace the last line of your script with:

IFS=$'\n'
echo $(find ${SRCROOT} -name "AppIcon60x60@2x.png")
  1. The first line temporarily sets the IFS-internal field separator-to the newline character. If you don’t do this, the second line will fail because the file name, Llama Trot, contains a space-try commenting out the first line if you’re curious about what happens.
  2. The second line in this command searches the ${SRCROOT} folder recursively for the file AppIcon60x60@2x.png.

By the way, it’s important that you type the line IFS=$'\n' precisely as it appears–no spaces on either side of the = sign.

Run the project, and you’ll see the full path to AppIcon60x60@2x echoed out:

IconLocation

Putting It All Together

The hard work is over. Now it’s time to put everything together and get your script to modify the app icons properly. You’ll start by only modifying AppIcon60x60@2x.png, and then work towards generalizing it for all of the app icons. This means you’ll need to test with a retina @2x iPhone simulator or device-so not the iPhone 6+.

By combining the techniques from ImageMagick and your previous script, you end up with the following script. Make sure to update your script accordingly:

IFS=$'\n'
#1
PATH=${PATH}:/usr/local/bin
 
#2
TARGET_PATH="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppIcon60x60@2x.png" 
BASE_IMAGE_PATH=$(find ${SRCROOT} -name "AppIcon60x60@2x.png") 
 
#3
convert betaRibbon.png -resize 120x120 resizedBetaRibbon.png 
 
#4
convert ${BASE_IMAGE_PATH} -fill white -font Times-Bold -pointsize 18 -gravity south -annotate 0 "Hello World" - | composite resizedBetaRibbon.png - ${TARGET_PATH}

Here’s a breakdown of what’s happening:

  1. The build will fail if you omit this line. Your terminal has a variable called PATH that stores a list of default script locations. Terminal looks there first for any commands that aren’t a part of Unix by default. This allows any command located in a directory in PATH to run without specifying the full location of the command. Xcode needs to share the same PATH variable as your terminal. This line adds /user/local/bin to the PATH variable where Homebrew installs its packages.
  2. The next two lines get the location of the app icons, as before, and then they save each path into variables TARGET_PATH and BASE_IMAGE_PATH respectively.
  3. This line sizes the beta ribbon down to the appropriate size
  4. The last line does two things at once. First, it annotates the initial app icon with the text Hello World. The script then pipes into the composite function — that’s what the | symbol is all about — and places the resized beta ribbon on top of it. ImageMagick does this by using a dash “-” instead of the file name. The script saves the result as the product app icon.

Note: App icon names are not arbitrary. The name of the app icons must be like AppIcon60x60@2x.png in the final product. Xcode uses this naming convention to determine which icon to use based on which device is in use.

Run the app and go to your device’s home screen to see the icons for your app; if you’re on the simulator, press Cmd + Shift + H to get to the home screen. You should see a modified icon:

AppIcon60x60@2x

The Rest of the Icons

Now that you’ve done quite a bit with one icon, it’s time to generalize the script to accomodate all your icons, so it will work with iPad icons, the iPhone 6+ icon etc.

To do this, you’ll make the icon modification code into a function that takes the icon’s name as an argument. Then execute that function for every icon that you have.

Modify the script so it looks like the following:

PATH=${PATH}:/usr/local/bin
IFS=$'\n'
 
function generateIcon () {
  BASE_IMAGE_NAME=$1
 
  TARGET_PATH="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${BASE_IMAGE_NAME}"
  BASE_IMAGE_PATH=$(find ${SRCROOT} -name ${BASE_IMAGE_NAME})
 
  WIDTH=$(identify -format %w ${BASE_IMAGE_PATH})
 
  convert betaRibbon.png -resize $WIDTHx$WIDTH resizedRibbon.png
  convert ${BASE_IMAGE_PATH} -fill white -font Times-Bold -pointsize 18 -gravity south -annotate 0 "Hello World" - | composite resizedRibbon.png - ${TARGET_PATH}
}
 
generateIcon "AppIcon60x60@2x.png"
generateIcon "AppIcon60x60@3x.png"
generateIcon "AppIcon76x76~ipad.png"
generateIcon "AppIcon76x76@2x~ipad.png"

This puts the entire image processing code into a function called generateIcon(), and you pass the name of the icon to process as an argument. The script accesses this argument by using $1, and sets it to the variable BASE_IMAGE_PATH. ${BASE_IMAGE_PATH} then takes the place where AppIcon60x60@2x.png used to be.

You’ll also notice a new ImageMagick function, identify, and this function gets information about an image. In this case, you want to use the width that you obtain with the -format %w parameter on identify to determine how to resize betaRibbon.png.

Now, switch to an iPad or an iPhone 6+ simulator and run the project; you’ll see the modified app icons on the home screens.

You’ll see that on the new devices, the font size appears to be inconsistent. This is because the font size is expressed in pixels, and different device screens have different pixel densities.

IconsBefore

There’s an easy fix for this. All you really want is the height of the text to be a certain proportion to the height of the entire icon.

Add the following line to your script immediately after where you set the WIDTH variable:

FONT_SIZE=$(echo "$WIDTH * .15" | bc -l)

This line is a little tricky, but what it does is set a FONT_SIZE variable to be one-fifth of the WIDTH variable. Since Unix arithmetic doesn’t support floating point arithmetic, you must use the bc program.

Short for basic calculator, bc can do floating point calculation. Since it’s a stand-alone program, the string $WIDTH * .15 needs to piped in for it to execute what you want.

Now, change last line of generateIcon() to use the value of FONT_SIZE instead of the hard-coded value of 18. The resulting script looks like this:

PATH=${PATH}:/usr/local/bin
IFS=$'\n'
 
function generateIcon () {
  BASE_IMAGE_NAME=$1
 
  TARGET_PATH="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${BASE_IMAGE_NAME}"
  BASE_IMAGE_PATH=$(find ${SRCROOT} -name ${BASE_IMAGE_NAME})
 
  WIDTH=$(identify -format %w ${BASE_IMAGE_PATH})
  FONT_SIZE=$(echo "$WIDTH * .15" | bc -l)
 
  convert betaRibbon.png -resize $WIDTHx$WIDTH resizedRibbon.png
  convert ${BASE_IMAGE_PATH} -fill white -font Times-Bold -pointsize ${FONT_SIZE} -gravity south -annotate 0 "Hello World" - | composite resizedRibbon.png - ${TARGET_PATH}
}
 
generateIcon "AppIcon60x60@2x.png"
generateIcon "AppIcon60x60@3x.png"
generateIcon "AppIcon76x76~ipad.png"
generateIcon "AppIcon76x76@2x~ipad.png"

Run your project on various devices, and you’ll see that things look much better.

IconsAfter

Goodbye World, Hello Build Number

As tempting as it’s to leave the Hello World text as an homage to the early days, you want to put the build number on these icons, and actually, it’s a pretty easy thing to do.

The build number can be found as the CFBundleVersion entry in the Info.plist file of your project.

So how are you going to get it into your script? As it turns out, your Mac came with a program to help you do this. It’s called PlistBuddy, and you’ll find it in /usr/libexec/.

Add the following line to the very top of your script:

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")

This line shows how to use PlistBuddy to get your build number. Now simply replace the “Hello World” part of your script with $buildNumber:

convert ${BASE_IMAGE_PATH} -fill white -font Times-Bold -pointsize ${FONT_SIZE} -gravity south -annotate 0 "$buildNumber" - | composite resizedRibbon.png - ${TARGET_PATH}

In the General tab of your target’s settings, change your Build number to 2015:

BuildNumber

Now run the project. You’ll see an icon with the corresponding build number:

AppendedBuildNumber

Build Configurations

Cool! The full loop is complete. You’ve’ overlaid a beta ribbon and the build number to the app icon and it runs every time you build your project.

But you don’t want a beta ribbon and build number all the time. What if you’ve got an alpha build? Or more likely, what if you’re releasing a version and have a release build? You definitely don’t want to put the build number on that.

That’s where Build Configurations come in.

In Xcode, go to your project’s configurations. You should see two configurations by default: Debug and Release.

Press the +, choose Duplicate Release and rename it to Beta. This creates a new build configuration setup that is exactly the same as the Release configuration.

Screen Shot 2015-05-10 at 7.50.48 PM

The Debug configuration will be the alpha/debug version, the Beta configuration will be the beta version, and the Release configuration will be the release version of the app.

Now all you need to do is pull the configuration into your script with the CONFIGURATION build settings variable, and add an if statement into your script determine to the current configuration. Update your script to the following:

IFS=$'\n'
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
versionNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${PROJECT_DIR}/${INFOPLIST_FILE}")
PATH=${PATH}:/usr/local/bin
 
function generateIcon () {
  BASE_IMAGE_NAME=$1
 
  TARGET_PATH="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/${BASE_IMAGE_NAME}"
  echo $TARGET_PATH
  echo $SRCROOT
  echo $(find ${SRCROOT} -name "AppIcon60x60@2x.png")
  BASE_IMAGE_PATH=$(find ${SRCROOT} -name ${BASE_IMAGE_NAME})
  WIDTH=$(identify -format %w ${BASE_IMAGE_PATH})
  FONT_SIZE=$(echo "$WIDTH * .15" | bc -l)
  echo "font size $FONT_SIZE"
 
  if [ "${CONFIGURATION}" == "Debug" ]; then
  convert debugRibbon.png -resize ${WIDTH}x${WIDTH} resizedRibbon.png
  convert ${BASE_IMAGE_PATH} -fill white -font Times-Bold -pointsize ${FONT_SIZE} -gravity south -annotate 0 "$buildNumber" - | composite resizedRibbon.png - ${TARGET_PATH}
  fi
 
  if [ "${CONFIGURATION}" == "Beta" ]; then
  convert betaRibbon.png -resize ${WIDTH}x${WIDTH} resizedRibbon.png
  convert ${BASE_IMAGE_PATH} -fill white -font Times-Boldr -pointsize ${FONT_SIZE} -gravity south -annotate 0 "$buildNumber" - | composite resizedRibbon.png - ${TARGET_PATH}
  fi
}
 
generateIcon "AppIcon60x60@2x.png"
generateIcon "AppIcon60x60@3x.png"
generateIcon "AppIcon76x76~ipad.png"
generateIcon "AppIcon76x76@2x~ipad.png"

Here’s what the changes enable:

  • If the configuration is Debug, the script uses a debugRibbon.png.
  • If the configuration is Beta, the script uses betaRibbon.png like before.
  • If the configuration is Release, the script won’t do anything because the app icon should remain unmodified.

To change the build configuration, Choose Product\Scheme\Edit Scheme… select Info and then choose the build configuration based on which action you’ll do e.g., Run, Archive, Profile, etc.

Screen Shot 2015-05-11 at 1.51.34 AM

Changing the build configuration for Run will reflect in your testing.

And that’s it! You have build numbers and ribbons for the debug and beta version of you app!

AppIcon60x60@3x

App2Beta

Where To Go From Here?

This tutorial was pretty heavy in scripting, so if you got through it all, congrats!

You can get the version number of your project with /usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString".

You might want to use that to provide the version number in addition to the build number on your icon.

There are numerous conveniences where Build Scripts can help you with all kinds of jobs. Here are some of my favorites:

  • Bump the build number every time an archive is made.
  • Automated git tagging
  • Generating seed databases before an app launch

Thanks for working through this tutorial today! You covered quite a bit and now know how to keep your various builds organized and clearly labeled for yourself and any testers you’re working with. And you also learned how to use Build Scripts and ImageMagick to resize and mark up images based on your own parameters. Talk about power!

Please feel free to share your questions, key learnings and any other ideas you have for using Build Scripts in the forums below.

The post How To Change Your App Icon at Build Time appeared first on Ray Wenderlich.


Video Tutorial: Introducing iOS 9 Search APIs Part 2: Core Spotlight

Video Tutorial: Introducing iOS 9 Search APIs Part 3: Handling Result Selection

Video Tutorial: Introducing iOS 9 Search APIs Part 4: Conclusion

Video Tutorial: Beginning Unity VR Part 1: Getting Started

Swift Expanding Cells in iOS Collection Views

$
0
0
Finalstep

Expanding collection view cells!

When collection views were introduced in 2012, many people thought it was the answer to the roadblocks and limitations of customizing table views.

Out of the box, UICollectionView is a powerful and ultra-flexible way to present data in your apps. However, one downside is that without some level of customization, your app looks bland and fails to stand out among the millions of apps in the App Store.

In this tutorial, you’re going to take a rather plain “template” app of different colored items and turn it into a visually appeasing way to browse RWDevCon inspiration talk sessions.

You’ll add awesome parallax effects and a featured cell item, as well as dabble in subtle fading and transitions to make it really pop and stand out from the crowd. The end result is a look and feel similar to the UltraVisual app. If you haven’t seen it, get it now – it’s free!

Before beginning, you should know how to work with UICollectionViewController, or its close cousin UITableViewController, and understand the basics of Swift. If you need a refresher, check out our two-part UICollectionView tutorial for the basics.

Note from Ray: This is a written version of the Video Tutorial Series, Custom Collection View Layouts from RayWenderlich.com team member, Mic Pringle!

Getting Started

Download the Ultravisual Starter Kit project and open it in Xcode 6. Inside, you’ll find a simple storyboard project with several classes conveniently separated into their respective folders in order to remove some of the boilerplate code and setup details to get the app running.

The folders are broken out as follows:

  • Assets: Contains Images.xcassets, Inspirations.plist and Inspirations.xcassets, which are used to load the images for the RWDevCon speakers.
  • Controllers: Contains InspirationsViewController.swift, a UICollectionViewController subclass that sets up the collection view in the same way UITableViewController does.
  • Extensions: Contains UIColor+Palette.swift and UIImage+Decompression.swift, which provide convenience methods that handle color conversion and image decompression, respectively. The starter project uses the color palette to start off with, and in a future step, you’ll switch to using images and the decompression method.
  • Layouts: Contains UltravisualLayout.swift — the meat of the project. As a subclass of UICollectionViewLayout, UICollectionView will look to this class for a definition of how to properly lay out and place the items specified and provided by InspirationsViewController. Inside, you’ll also find a set of constants and convenience methods you’ll use to pre-calculate and simplify set up of the starter project. Additionally, a simple custom cache has been setup to cache UICollectionViewLayoutAttributes that are used to modify each cell.
  • Models: Contains the data models for the background images (Inspiration.swift) and session details (Session.swift); they are separated for the ultimate MVC-pattern win!
  • Views: Contains InspirationCell.swift, which handles setting the properties of the collection view cell, just like UITableViewCell.

Build and run the starter project, and you’ll see the following:

Starter Kit Screenshot

The app looks decent (if you want a random color palette app, that is), but it doesn’t do much right now. That’s where your job comes in — you’ll turn this clunker into a beautiful app that’s designed to quickly scan through the list of inspirational speaker topics from RWDevCon.

The technical magic of this project comes from managing the different cell item states:

  • The standard cell (what you see in the starter project) that transitions into the “next up” cell which grows a bit larger
  • The featured cell, which is the largest of all.

You’ll also use a little trickery — modifying the z-index of the cell as it scrolls — for a sweet stacking effect.

And now it’s time to stretch your fingers and clear your mind in preparation of the amazing work you’re about to do. That’s right boys and girls, it’s…

Multiple Cell Sizes

The first thing you need to do is create the featured cell. It’ll be roughly twice as large as the standard cell, so a user can clearly see which item is selected.

Jump to UltravisualLayout.swift and go to prepareLayout(). After this line of code:

cache.removeAll(keepCapacity: false)

Add a few new local variables that you’ll use across each item:

let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight
 
var frame = CGRectZero
var y: CGFloat = 0

Next, add the following code to loop through each item in the collection view and adjust the item’s state accordingly:

for item in 0..<numberOfItems {
  // 1
  let indexPath = NSIndexPath(forItem: item, inSection: 0)
  let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
 
  // 2
  attributes.zIndex = item
  var height = standardHeight
 
  // 3
  if indexPath.item == featuredItemIndex {
    // 4
    let yOffset = standardHeight * nextItemPercentageOffset
    y = collectionView!.contentOffset.y - yOffset
    height = featuredHeight
  } else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems {
    // 5 
    let maxY = y + standardHeight
    height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 0)
    y = maxY - height
  }
 
  // 6
  frame = CGRect(x: 0, y: y, width: width, height: height)
  attributes.frame = frame
  cache.append(attributes)
  y = CGRectGetMaxY(frame)
}

There is a lot going on in the loop so here’s a breakdown:

  1. Create an index path to the current cell, then get its current attributes.
  2. Prepare the cell to move up or down. Since the majority of cells will not be featured — there are many more standard cells than the single featured cells — it defaults to the standardHeight.
  3. Determine the current cell’s status — featured, next or standard. In the case of the latter, you do nothing.
  4. If the cell is currently in the featured cell position, calculate the yOffset and use that to derive the new y value for the cell. After that, you set the cell’s height to be the featured height.
  5. If the cell is next in line, you start by calculating the largest y could be (in this case, larger than the featured cell) and combine that with a calculated height to end up with the correct value of y, which is 280.0 — the height of the featured cell.
  6. Lastly, the loop sets some common elements for each cell, including creating the right frame based upon the if condition above, setting the attributes to what was just calculated, and updating the cache values. The very last step is to update y so that it’s at the bottom of the last calculated cell, so you can move down the list of cells efficiently.

For the collection view to know which layout to use, you’ll need to tell UICollectionView to use UltravisualLayout as its layout class. Open Main.storyboard and select the Collection View inside of Document Outline.

Collection View Selection

Open the Attributes Inspector and set the Layout dropdown to Custom, and then set the Class to UltravisualLayout.

Screen Shot 2015-05-17 at 5.05.37 PM

Finally, before building, go to InspirationsViewController.swift and in viewDidLoad(), remove the last 2 lines:

let layout = collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: CGRectGetWidth(collectionView!.bounds), height: 100)

These two lines were the basic layout to show the equally sized multi-colored cells. Now that you’ve specified layout code in UltravisualLayout and set the custom layout class in the storyboard, you no longer need these lines.

Build and run, and you’ll see this:

Step1gif

Notice that the top cell is much larger, effectively showcasing a featured cell. As you scroll, the cell below the featured cell expands and overlaps the current featured cell. What a nice effect!

Adding Cell Images

Colored cells are great and all, but RWDevCon cannot be expressed in color!

There is a set of images included in the starter project just begging to be used. PS: they’re in Inspirations.xcassets if you want to steal a quick glance. :]

But you can’t just use them in any old way — if you’re doing this tutorial, then clearly you’re all about that awesome user experience.

Not only is it time to replace the colors with images, but you’ll do so in such a way that the image actually reveals more of itself as the user scrolls the cell up to be a featured cell.

First, add a new image view to the collection view cell. Go to Main.storyboard, select InspirationCell, and resize it to 200×200. This is not strictly necessary but it helps you visually see what’s going on more easily than the standard cell size does.

Screen Shot 2015-05-17 at 10.50.30 PM

Next, open the Identity Inspector and set the Class type to be the included InspirationCell.

Screen Shot 2015-05-17 at 10.53.55 PM

Then drag and drop a UIImageView object from the Object Library onto the cell. Select the UIImageView you just added, go to the Attributes Inspector and change the Mode to Aspect Fill.

Now for a few Auto Layout constraints — select the Pin menu at the bottom of the layout view and set up the constraints as follows:

  • Uncheck Constrain to Margins
  • Set the leading and trailing spaces (the left and right boxes) to 0
  • Check Height constraint and set its value to 280 (the height of the featured cell)
  • In the Update Frames dropdown, select Items of New Constraints
  • Click Apply 3 New Constraints

It should appear something like this:

Screen Shot 2015-05-17 at 11.05.45 PM

From the Align menu (located to the left of the Pin menu), select the Vertical Center in Container option and apply that single constraint.

This creates a cool effect that centers the image in the image view in proportion to the full height of the featured cell, but it appears masked by the standard cell height until the cell is fully in view.

Screen Shot 2015-05-17 at 11.03.33 PM

Finally, add an outlet connection by right-clicking on InspirationCell in the Document Outline and connecting the imageView outlet to the UIImageView you just added.

Screen Shot 2015-05-17 at 11.08.29 PM

Jump over to InspirationsViewController.swift and update the implementation of collectionView(_:cellForItemAtIndexPath:) to the following:

let cell = collectionView.dequeueReusableCellWithReuseIdentifier("InspirationCell", forIndexPath: indexPath) as! InspirationCell
cell.inspiration = inspirations[indexPath.item]
 
return cell

Since you set the cell’s class in Interface Builder, you start by casting to InspirationsCell in the dequeue call. Rather than set the cell’s background color, you set the cell’s inspiration property to the correct piece of data from the array of all inspirations.

Lastly, at the top of the file, remove the colors array because you won’t be needing it anymore.

Build and run, and behold pure image glory!

iOS Simulator Screen Shot May 17, 2015, 11.16.04 PM

Featuring Featured Cells

The new images look great, but one thing you’ll notice is that all of the images are competing for your attention regardless of where they are in the list.

In reality, only the featured cell should grab your attention.

Next, you’ll add a simple UIView overlay to manage the opacity of each cell. This time, the trickery you’ll use is a mask to darken cells as they move away from the featured view and lighten as they get closer to the top.

Go back to Main.storyboard and add a UIView on to the UIImageView you added previously.

Screen Shot 2015-05-18 at 12.15.33 AM

In the Pin auto layout menu, uncheck Constrain to Margins, and this time, set all four spaces to 0. Select Items of New Constraints for the Update Frames dropdown and apply the four new constraints.

Screen Shot 2015-05-18 at 12.17.11 AM

Next, hook up the imageCoverView outlet from InspirationCell to the UIView you just added. Select the view again, and in Attributes Inspector set the Background Color to Black Color.

Now that you’ve set up the UI to mask the images, it’s time to update the mask in code.

Go to InspirationCell.swift in the Views folder, and at the bottom of the file, add the following method:

override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
  super.applyLayoutAttributes(layoutAttributes)
 
  // 1
  let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
  let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight
 
  // 2
  let delta = 1 - ((featuredHeight - CGRectGetHeight(frame)) / (featuredHeight - standardHeight))
 
  // 3
  let minAlpha: CGFloat = 0.3
  let maxAlpha: CGFloat = 0.75
  imageCoverView.alpha = maxAlpha - (delta * (maxAlpha - minAlpha))
}

This method updates the effects applied to the cell as it’s rendering and moving up or down the screen. Here’s what’s happening step-by-step:

  1. These are the two convenience height constants you’ve used previously.
  2. Calculate the delta of the cell as it’s moving to figure out how much to adjust the alpha in the following step.
  3. Based on the range constants, update the cell’s alpha based on the delta value.

The delta is calculated as a range of 0 to 1 as it determines the percentage of the height change from the standard height to the featured height cells. From there, the alpha can be calculated as the percentage delta is applied to the acceptable alpha range, which is between 0.3 and 0.75 in this case.

Build and run!

Alpha

Adding Session Details

At this point, you’ve got the images looking great with a subtle parallax effect and alpha transparency, and you’re probably feeling about ready to take a victory lap around your workstation.

I won’t stop you, but before you get too excited, keep in mind that there’s just one problem; without the context of the session talk, time and room, the images are little more than pretty pictures!

You’ll add those bits next.

Note: This next section is directly from the provided answer to the challenge section of the video series. If you want to try and solve the problem yourself, stop here and read the first page in the Hands-on Challenge from the video post. If you’ve followed the written tutorial thus far, you’re in the same place as where the video tutorial leaves you.

First, add the session title to each item. Open Main.storyboard and drag a UILabel from the Object Library onto the InspirationCell in the Document Outline. Make sure the label is created as a sibling, meaning it’s on the same level in the hierarchy, of Image View and View.

InspectorStep1

Select the label, and in the Attributes Inspector make the following changes:

  • Set Text to “Inspiration”
  • Set Color to White
  • Set Font to Custom, Family to Avenir Next, with a Style of Demi Bold and a Size of 38
  • Set Alignment to Center

InspectorStep2

With the label still selected, click the Pin button in the bottom right-hand corner of Interface Builder. In the pop-up, make sure Constrain to margins is unchecked, and then add the following layout constraints:

  • Select Leading Space and set the constant to 0
  • Select Trailing Space and set the constant to 0
  • Set Update Frames to Items of New Constraints
  • Click Add 2 Constraints

Screen Shot 2015-05-22 at 11.46.38 PM

Finally, click the Align button, select Vertical Center in Container, change Update Frames to Items of New Constraints and click Add 1 Constraint:

Screen Shot 2015-05-17 at 11.03.33 PM

Your cell should now look like this:

Screen Shot 2015-05-22 at 11.47.04 PM

Next, you’ll have to hook up the new label to the cell so you can set the text correctly. Open InspirationCell.swift and add a new property at the top of the file:

@IBOutlet private weak var titleLabel: UILabel!

Then, inside of the if block of didSet observer for inspiration add:

titleLabel.text = inspiration.title

This ensures the text of the label updates each time the value of the property changes.

Jump back to Main.storyboard and right-click on InspirationCell in the Document Outline to show the connections popup. Drag from titleLabel to the label you just created to connect the two:

InspectorStep4

Build and run, then check out those sweet, sweet session titles!

Screen Shot 2015-05-22 at 11.47.27 PM

You know what would be a cool effect on the title labels? Having them scale as the user scrolls!

Cell Scaling

Open InspirationCell.swift and add the following to the bottom of applyLayoutAttributes(_:):

let scale = max(delta, 0.5)
titleLabel.transform = CGAffineTransformMakeScale(scale, scale)

Here you create a constant that’s the greater of either delta (which, if you remember, is a value between 0 and 1) and 0.5 — this is important because you don’t want the label to scale to less than half its full size.

Next you use CGAffineTransformMakeScale(_:_:) to create a scaled transform, and set it on the label via its transform property.

Build and run. You’ll see that the session titles in the standard cells are half the size of the title in the featured cell, and that the label smoothly scales as a standard cell transitions into being the featured cell:

Screen Shot 2015-05-22 at 11.47.39 PM

Now it’s time to add the remainder of the session details to the cell and make them fade into view as the user scrolls.

Adding Session Details

Open Main.storyboard and drag two more Labels from the Object Library onto the cell in the Document Outline – again, make sure they’re created as siblings of the other views and not as children.

The updated Document Outline should look like the following:

Screen Shot 2015-05-22 at 11.47.50 PM

Using the Attributes Inspector, make the following changes to both of the new labels:

  • Set Text of one label to “Time, Room” and the other to “Speaker”
  • Set Color to White
  • Set Font to Custom, Family to Avenir Next, with a Style of Medium and a Size of 17
  • Set Alignment to Center

In the storyboard canvas, drag both labels down so that they appear below the large Inspiration label.

Position the Time, Room label below the Inspirations label. Put the Speaker label below the Time, Room label so that they are appear stacked atop one another.

Select the Time, Room label and click the Pin button. Uncheck Constrain to margins and then add the following layout constraints:

  • Select Leading and set the constant to 0
  • Enable Top and set the constraint to 0 — make sure it’s referencing Inspiration and not InspirationCell by clicking on the dropdown arrow
  • Enable Trailing and set the constant to 0
  • Make sure Update Frames is set to Items of New Constraints
  • Click Add 4 Constraints

Now select the Speaker label and add the same layout constraints, but this time make sure the Top space is referencing Time, Room and not Inspiration or InspirationCell.

If you have difficulty getting the right value to come up in the dropdown, try nudging the label further down the view a bit, say, 10px, and look at the dropdown again. The view has to be sufficiently spaced so that Interface Builder can accurately detect what you’re trying to do.

Your cell should now look like the following:

Screen Shot 2015-05-22 at 11.47.57 PM

Jump back to InspirationCell.swift and add the following outlets, just below the others:

@IBOutlet private weak var timeAndRoomLabel: UILabel! 
@IBOutlet private weak var speakerLabel: UILabel!

These will be used to set the Time, Room, and Speaker details on their corresponding labels.

Now, add the following to the bottom of the if block inside the didSet observer for the inspiration property:

timeAndRoomLabel.text = inspiration.roomAndTime 
speakerLabel.text = inspiration.speaker

Just like the titleLabel you set up previously, this ensures the values update whenever the inspiration values change.

Jump back to Main.storyboard and right-click on InspirationCell in the Document Outline to invoke the connections popup.

Drag from timeAndRoomLabel to the top label to connect them, and then drag from speakerLabel to the bottom label to connect those two.

Build and run. You’ll see that the session details now display as expected, but they don’t look quite right in the standard cells:

Screen Shot 2015-05-22 at 11.48.04 PM

Time to fix that!

Find applyLayoutAttributes(_:) in InspirationCell.swift and add the following to the very bottom:

timeAndRoomLabel.alpha = delta 
speakerLabel.alpha = delta

You’re simply setting the alpha of each label to the delta variable, which, if you remember, represents the progress of the cell transitioning to be the featured cell.

By doing this, you’re making sure the session details aren’t visible in standard cells, but fade in as each cell becomes the featured cell.

Once again, build and run. You’ll now see that session details aren’t displayed on the standard cells but instead fade in as you scroll:

Screen Shot 2015-05-22 at 11.48.13 PM

What a thing of beauty! :]

Smooth Scrolling

There’s one last thing you have to do to make this app’s UI really pop; you’re going to tweak the way the collection view scrolls so that one cell is always in full focus at the top of the screen. This will help your users focus on the content, rather than spending energy on scrolling to an exact position.

Open UltravisualLayout.swift and add the following method to the class:

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
  let itemIndex = round(proposedContentOffset.y / dragOffset)
  let yOffset = itemIndex * dragOffset
  return CGPoint(x: 0, y: yOffset)
}

This is a little-used method of UIScrollView that allows your app to respond with an effect similar to the page snapping effect of a paged UIScrollView.

When the user lifts their finger after a scroll while there’s still some scroll velocity, this method will look into the future and tell you exactly where the scroll will end thanks to the proposedContentOffset. By returning a different scroll point, you can make the collection view end up right on an even boundary with a featured cell.

All you’re doing in the implementation is finding the closest item to the proposed content offset, and then returning a CGPoint that’s positioned so that item will be right against the top of the screen.

By default, a scroll view’s deceleration rate is rather slow, which can make your target offset change look a bit strange. That’s an easy fix though! Jump to InspirationsViewController.swift, and in viewDidLoad() add the following code to the end of the method:

collectionView!.decelerationRate = UIScrollViewDecelerationRateFast

Build, run and you’re done. :]

FinalKitGif

Challenge: Tap to Select

Oh, there’s one more thing. It’s pretty cool to be able to scroll that list around, but did you notice what happens when you tap a cell?

That’s right, nothing. #boring.

Why not scroll a tapped cell to the top so it gains focus? Take a shot at adding a new function to handle tapping events. Think about how you would handle selecting a row on a UITableView for a similar treatment.

Solution Inside SelectShow>

How close was your own code to the example solution?

Where to Go From Here

You can download the final project with all the code from this tutorial to check out the finished app.

In this tutorial, you added several auto layout-based labels, subtle parallax effects, and fading transitions to a standard Collection View.

Keeping class responsibilities focused — especially for the view controllers — is one of the main challenges in iOS development, and you handled them like a boss in this tutorial.

Now that you’ve covered the basics of adding a fancy expanding cell to UICollectionView, try experimenting with 3D transforms or fiddling with the scaling values in the collection layout to see what other effects you can achieve.

As mentioned, this tutorial is based on our Custom Collection View Layout video tutorial series. There are four custom collection view layouts in that series and this was just one of them — check out the rest of the videos for even more collection view fun!

If you have any questions about this tutorial, please join in the forum discussion below!

The post Swift Expanding Cells in iOS Collection Views appeared first on Ray Wenderlich.

Video Tutorial: Beginning Unity VR Part 2: Oculus SDK

Podcasting with Greg Heo – Podcast S04 E09

$
0
0
Learn how to run or be a guest on a podcast, with Greg Heo!

Learn how to run or be a guest on a podcast, with Greg Heo!

Learn how to run or be a guest on a podcast, with Greg Heo!

[Subscribe in iTunes] [RSS Feed]

Our Sponsor

  • Envato Bundles: 6 beautiful Mac Apps including Macaw, Arq Backup, Ember, RapidWeaver, ColourSnapper and Amberlight to help inspire your designs and streamline your workflow.

Links

Mic and Jake’s Podcast Picks

  • Amplified – Mic’s podcast pick #1
  • Podcast method – Mic’s podcast pick #2
  • NSBrief – Mic’s podcast pick #3
  • ATP – Jake’s podcast pick #1
  • The Nerdist – Jake’s podcast pick #2
  • rw.com Team – Hosting Podcasts

    rw.com Team – Guests on Podcasts

    Contact Us

    Where To Go From Here?

    We hope you enjoyed this episode of our podcast. Be sure to subscribe in iTunes to get notified when the next episode comes out.

    We’d love to hear what you think about the podcast, and any suggestions on what you’d like to hear next season. Feel free to drop a comment here, or email us anytime at podcast@raywenderlich.com.

    The post Podcasting with Greg Heo – Podcast S04 E09 appeared first on Ray Wenderlich.


    RWDevCon 2016: First Batch of Sponsors Announced!

    $
    0
    0

    RWDevCon

    As you may have heard, 360iDev is running this week – if you’re there, be sure to say hi to the raywenderlich.com team members who are speaking!

    As you also may have heard, we are teaming up with the folks from 360iDev to host another iOS developer conference next March: RWDevCon 2016.

    RWDevCon is the first ever tutorial conference. That means instead of just passively watching talks, you get to participate – via hands-on tutorials!

    You’ll play around with iOS 9, Swift 2, watchOS 2, and app development in general, along with the authors of the books, tutorials, and videos you know and love from this site.

    And best of all – you won’t just have a hodge-podge of random talks. Instead, RWDevCon will be coordinated as a team so you have an amazing experience.

    I’ll be giving you periodic updates of how things are proceeding with the conference. This week, I’m pleased to announce our first batch of sponsors for the conference!

    6 New Sponsors!

    Today, we are happy to announce 6 new sponsors for the conference:

    rwdevcon_sponsors1

    • Couchbase Mobile (platinum sponsor): Couchbase Mobile is a NoSQL database solution that delivers the full power and flexibility of NoSQL to mobile. It’s engineered to provide fast and consistent access to your data, with or without a network connection, removing the network dependency that traditional service-based approaches require.
    • Citrix (gold sponsor): Citrix securely delivers desktops, apps, and services with a single integrated solution over any network.
    • Google Firebase (gold sponsor): Firebase powers your app’s backend, including data storage, user authentication, static hosting, and more.
    • AppCode (bronze sponsor): AppCode is an intelligent IDE that helps iOS/OSX developers create outstanding apps with ease and pleasure. Now with Swift support!
    • Count.ly (bronze sponsor): Count.ly makes it easy to integrate analytics into your apps, with real-time reporting and a sweet web-based dashboard. This is a great way to see what your users are doing and improve your app accordingly.
    • Reveal (bronze sponsor): Reveal brings powerful runtime view debugging to iOS developers. With advanced visualizations, comprehensive inspectors and the ability to modify applications on the fly, you’ll be debugging view layout and rendering problems in seconds.

    Huge thanks to Couchbase Mobile, Citrix, Google Firebase, AppCode, Count.ly, and Reveal for being a part of RWDevCon!

    Get Your Ticket!

    Save yourself $100 by signing up now!

    This early-bird discount will only last for a short time, so be sure to grab it while you still can.

    The team and I look forward to meeting you at RWDevCon for some tutorials, inspiration, and fun!

    The post RWDevCon 2016: First Batch of Sponsors Announced! appeared first on Ray Wenderlich.

    UICollectionView Custom Layout Tutorial: Pinterest

    $
    0
    0
    create custom layouts

    Create awesome user interfaces with collection views and custom layouts!

    UICollectionView, introduced in iOS 6, has become one of the most popular UI elements among iOS developers. What makes it so attractive is the separation between the data and presentation layers, which depends upon a separate object to handle the layout. The layout is then responsible for determining the placement and visual attributes of the views.

    You’ve likely used the default flow layout — a layout class provided by UIKit — which consists of a basic grid layout with some customizations. But you can also implement your own custom layouts to arrange the views any way you like; this is what makes the collection view so flexible and powerful.

    In this tutorial, you’ll create a UICollectionView custom layout inspired by the popular Pinterest app.

    In the process, you’ll learn a lot about custom layouts, how to calculate and cache layout attributes, how to handle dynamically sized cells and much more.

    Note: This tutorial requires a basic knowledge of UICollectionView. If you’re not familiar with it, you can learn more about it in our written or video tutorial series:

    Ready to pimp-up your collection view? Read on!

    Getting Started

    Download the starter project for this tutorial and open it in Xcode.

    Build and run the project, and you’ll see the following:

    Starter Project image

    The app presents a gallery of pictures from RWDevCon. You can browse the photos and see how much fun the attendees had while at the conference.

    The gallery is built using a collection view with a standard flow layout. At first sight, it looks all right. But the layout design could certainly be improved; the photos don’t completely fill the content area and long annotations end up truncated.

    Creating Custom Collection View Layouts

    Your first step in creating a stunning collection view is to create a custom layout class for your gallery.

    Collection view layouts are subclasses of the abstract UICollectionViewLayout class; they define the visual attributes of every item in your collection view. The individual attributes are instances of UICollectionViewLayoutAttributes and contain the properties of each item in your collection view, such as the item’s frame or opacity.

    Create a new file inside the Layouts group. Select Cocoa Touch Class from the iOS\Source list. Name it PinterestLayout and make it a subclass of UICollectionViewLayout. Make sure the selected language is Swift and finally create the file.

    Next you’ll need to configure the collection view to use your new layout.

    Open Main.storyboard and select the Collection View in the Photo Stream View Controller Scene as shown below:

    storyboard_select_collection_view

    Next, open the Attributes Inspector. Select Custom in the Layout drop-down list and select PinterestLayout in the Class drop-down list:

    storyboard_change_layout

    Okay — time to see how it looks. Build and run your app:

    build_and_run_empty_collection

    collectionview empty meme

    Don’t panic! This is a good sign, believe it or not. This means the collection view is using your custom layout class. The cells aren’t shown because the PinterestLayout class doesn’t yet implement any of the methods involved in the layout process.

    Core Layout Process

    Take a moment to think about the collection view layout process, which is a collaboration between the collection view and the layout object. When the collection view needs some layout information, it asks your layout object to provide it by calling certain methods in a specific order:

    Layout lifecycle

    Your layout subclass must implement the following methods:

    • prepareLayout(): This method is called whenever a layout operation is about to take place. It’s your opportunity to prepare and perform any calculations required to determine the collection view size and the positions of the items.
    • collectionViewContentSize(): In this method, you have to return the height and width of the entire collection view content — not just the visible content. The collection view uses this information internally to configure its scroll view content size.
    • layoutAttributesForElementsInRect(_:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.

    Okay, so you know what you need to implement — but how do you go about calculating these attributes?

    Calculating Layout Attributes

    For this layout, you need to dynamically calculate the position and height of every item since you don’t know what the height of the photo or the annotation will be in advance. You’ll declare a protocol that will provide this position and height info when PinterestLayout needs it.

    Now, back to the code. Open PinterestLayout.swift and add the following delegate protocol declaration before the PinterestLayout class:

    protocol PinterestLayoutDelegate {
      // 1
      func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath, 
          withWidth:CGFloat) -> CGFloat
      // 2
      func collectionView(collectionView: UICollectionView, 
          heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat
    }

    This code declares the PinterestLayoutDelegate protocol, which has two methods to request the height of the photo (1) as well as the annotation (2). You’ll implement this protocol in PhotoStreamViewController shortly.

    There’s just one more thing to do before implementing the layout methods; you need to declare some properties that will help with the layout process.

    Add the following to PinterestLayout:

    // 1 
    var delegate: PinterestLayoutDelegate!
     
    // 2
    var numberOfColumns = 2
    var cellPadding: CGFloat = 6.0
     
    // 3
    private var cache = [UICollectionViewLayoutAttributes]()
     
    // 4
    private var contentHeight: CGFloat  = 0.0
    private var contentWidth: CGFloat {
    let insets = collectionView!.contentInset
      return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)
    }

    This code defines some properties you’ll need later on to provide the layout information. Here it is, explained step-by-step:

    1. This keeps a reference to the delegate.
    2. These are two public properties for configuring the layout: the number of columns and the cell padding.
    3. This is an array to cache the calculated attributes. When you call prepareLayout(), you’ll calculate the attributes for all items and add them to the cache. When the collection view later requests the layout attributes, you can be efficient and query the cache instead of recalculating them every time.
    4. This declares two properties to store the content size. contentHeight is incremented as photos are added, and contentWidth is calculated based on the collection view width and its content inset.

    You’re ready to calculate the attributes for the collection view items, which for now will consist of the frame. To understand how this will be done, take a look at the following diagram:

    customlayout-calculations

    You’ll calculate the frame of every item based on its column (tracked by xOffset) and the position of the previous item in the same column (tracked by yOffset).

    To calculate the horizontal position, you’ll use the starting X coordinate of the column the item belongs to, and then add the cell padding. The vertical position is the starting position of the prior item in that column, plus the height of that prior item. The overall item height is the sum of the image height, the annotation height and the content padding.

    You’ll do this in prepareLayout(), where your primary objective is to calculate an instance of UICollectionViewLayoutAttributes for every item in the layout.

    Add the following method to PinterestLayout:

    override func prepareLayout() {
      // 1
      if cache.isEmpty {
        // 2
        let columnWidth = contentWidth / CGFloat(numberOfColumns)
        var xOffset = [CGFloat]()
        for column in 0 ..< numberOfColumns {
          xOffset.append(CGFloat(column) * columnWidth )
        }
        var column = 0
        var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)
     
        // 3
        for item in 0 ..< collectionView!.numberOfItemsInSection(0) {
     
          let indexPath = NSIndexPath(forItem: item, inSection: 0)
     
          // 4
          let width = columnWidth - cellPadding * 2
          let photoHeight = delegate.collectionView(collectionView!, heightForPhotoAtIndexPath: indexPath, 
              withWidth:width)
          let annotationHeight = delegate.collectionView(collectionView!,
              heightForAnnotationAtIndexPath: indexPath, withWidth: width)
          let height = cellPadding +  photoHeight + annotationHeight + cellPadding
          let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
          let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
     
          // 5
          let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
          attributes.frame = insetFrame
          cache.append(attributes)
     
          // 6
          contentHeight = max(contentHeight, CGRectGetMaxY(frame))
          yOffset[column] = yOffset[column] + height
     
          column = column >= (numberOfColumns - 1) ? 0 : ++column
        }
      }
    }

    Taking each numbered comment in turn:

    1. You only calculate the layout attributes if cache is empty.
    2. This declares and fills the xOffset array with the x-coordinate for every column based on the column widths. The yOffset array tracks the y-position for every column. You initialize each value in yOffset to 0, since this is the offset of the first item in each column.
    3. This loops through all the items in the first section, as this particular layout has only one section.
    4. This is where you perform the frame calculation. width is the previously calculated cellWidth, with the padding between cells removed. You ask the delegate for the height of the image and the annotation, and calculate the frame height based on those heights and the predefined cellPadding for the top and bottom. You then combine this with the x and y offsets of the current column to create the insetFrame used by the attribute.
    5. This creates an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
    6. This expands contentHeight to account for the frame of the newly calculated item. It then advances the yOffset for the current column based on the frame. Finally, it advances the column so that the next item will be placed in the next column.

    Note: As prepareLayout() is called whenever the collection view’s layout is invalidated, there are many situations in a typical implementation where you might need to recalculate attributes here. For example, the bounds of the UICollectionView might change – such as when the orientation changes – or items may be added or removed from the collection. These cases are out of scope for this tutorial, but it’s important to be aware of them in a non-trivial implementation.

    Next, add the following method to PinterestLayout:

    override func collectionViewContentSize() -> CGSize {
      return CGSize(width: contentWidth, height: contentHeight)
    }

    This overrides collectionViewContentSize() of the abstract parent class, and returns the size of the collection view’s contents. To do this, you use both contentWidth and contentHeight calculated in the previous steps.

    The last method you need to override is layoutAttributesForElementsInRect(_:), which the collection view calls after prepareLayout() to determine which items are visible in the given rect.

    Add the following code to the very end ofPinterestLayout:

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
     
      var layoutAttributes = [UICollectionViewLayoutAttributes]()
     
      for attributes  in cache {
        if CGRectIntersectsRect(attributes.frame, rect) {
          layoutAttributes.append(attributes)
        }
      }
      return layoutAttributes
    }

    Here you iterate through the attributes in cache and check if their frames intersect with rect, which is provided by the collection view. You add any attributes with frames that intersect with that rect to layoutAttributes, which is eventually returned to the collection view.

    Before you can see your layout in action, you need to implement the layout delegate. PinterestLayout relies upon this to provide photo and annotation heights when calculating the height of an attribute’s frame.

    Open PhotoStreamViewController.swift and add the following extension to the end of the file to implement the PinterestLayoutDelegate protocol:

    extension PhotoStreamViewController : PinterestLayoutDelegate {
      // 1
      func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath,
          withWidth width:CGFloat) -> CGFloat {
        let photo = photos[indexPath.item]
        let boundingRect =  CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
        let rect  = AVMakeRectWithAspectRatioInsideRect(photo.image.size, boundingRect)
        return rect.size.height
      }
     
      // 2
      func collectionView(collectionView: UICollectionView, 
          heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
        let annotationPadding = CGFloat(4)
        let annotationHeaderHeight = CGFloat(17)
        let photo = photos[indexPath.item]
        let font = UIFont(name: "AvenirNext-Regular", size: 10)!
        let commentHeight = photo.heightForComment(font, width: width)
        let height = annotationPadding + annotationHeaderHeight + commentHeight + annotationPadding
        return height
      }
    }

    Here’s what’s going on in the code above:

    1. This provides the height of the photos. It uses AVMakeRectWithAspectRatioInsideRect() from AVFoundation to calculate a height that retains the photo’s aspect ratio, restricted to the cell’s width.
    2. This calls heightForComment(_:width:), a helper method included in the starter project that calculates the height of the photo’s comment based on the given font and the cell’s width. You then add that height to a hard-coded annotationPadding value for the top and bottom, as well as a hard-coded annotationHeaderHeight that accounts for the size of the annotation title.

    Next, add the following code inside viewDidLoad(), just below the call to super:

    if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
      layout.delegate = self
    }

    This sets the PhotoStreamViewController as the delegate for your layout.

    Time to see how things are shaping up! Build and run your app. You’ll see the cells are properly positioned and sized based on the heights of the photos and the annotations:

    build_and_run_calculated_height

    You’re getting there, but the image view isn’t filling all the available space. You’ll fix that using custom layout attributes.

    Custom Layout Attributes

    You now need to resize the cell’s image view to match the calculated height of the photo. To do that, you need to create a subclass of UICollectionViewLayoutAttributes.

    By subclassing UICollectionViewLayoutAttributes, you can add your own properties, which are automatically passed to the cell. You can use these attributes by overriding applyLayoutAttributes(_:) in your UICollectionViewCell subclass, which your collection view calls during the layout process, as shown in the illustration below:

    layout-customattributes

    Open PinterestLayout.swift and add the following code add the top of the file, above the class declaration for PinterestLayout:

    class PinterestLayoutAttributes: UICollectionViewLayoutAttributes {
     
      // 1
      var photoHeight: CGFloat = 0.0
     
      // 2
      override func copyWithZone(zone: NSZone) -> AnyObject {
        let copy = super.copyWithZone(zone) as! PinterestLayoutAttributes
        copy.photoHeight = photoHeight
        return copy
      }
     
      // 3
      override func isEqual(object: AnyObject?) -> Bool {
        if let attributes = object as? PinterestLayoutAttributes {
          if( attributes.photoHeight == photoHeight  ) {
            return super.isEqual(object)
          }
        }
        return false
      }
    }

    This declares a UICollectionViewLayoutAttributes subclass named PinterestLayoutAttributes. Here’s how it works, step-by-step:

    1. This declares the photoHeight property that the cell will use to resize its image view.
    2. This overrides copyWithZone(). Subclasses of UICollectionViewLayoutAttributes need to conform to the NSCopying protocol because the attribute’s objects can be copied internally. You override this method to guarantee that the photoHeight property is set when the object is copied.
    3. This overrides isEqual(_:), and it’s mandatory as well. The collection view determines whether the attributes have changed by comparing the old and new attribute objects using isEqual(_:). You must implement it to compare the custom properties of your subclass. The code compares the photoHeight of both instances, and if they are equal, calls super to determine if the inherited attributes are the same. If the photo heights are different, it returns false.

    Next, add the following method to PinterestLayout :

    override class func layoutAttributesClass() -> AnyClass {
      return PinterestLayoutAttributes.self
    }

    This overrides layoutAttributesClass() to tell the collection view to use PinterestLayoutAttributes whenever it creates layout attributes objects.

    Next, you need to change the references to UICollectionViewLayoutAttributes in the layout class to PinteresLayoutAttributes.

    First, replace the following line:

    private var cache = [UICollectionViewLayoutAttributes]()

    with this one:

    private var cache = [PinterestLayoutAttributes]()

    Changing the reference to your new layout attributes introduces an error in prepareLayout().

    To fix it, replace this line:

    let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)

    with these two lines:

    let attributes = PinterestLayoutAttributes(forCellWithIndexPath: indexPath)
    attributes.photoHeight = photoHeight

    This creates an instance of PinterestLayoutAttributes and then assigns the photoHeight property that will be passed to the collection view cells.

    The last step is to change the image view height inside the cell.

    Open AnnotatedPhotoCell.swift and add the following method to the bottom of the class:

    override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
      super.applyLayoutAttributes(layoutAttributes)
      if let attributes = layoutAttributes as? PinterestLayoutAttributes {
        imageViewHeightLayoutConstraint.constant = attributes.photoHeight
      }
    }

    First, this code calls the super implementation to make sure that the standard attributes are applied. Then, it casts the attributes object into an instance of PinterestLayoutAttributes to obtain the photo height and then changes the image view height by setting the imageViewHeightLayoutConstraint constant value.

    Build and run your app. The contents of each cell should size properly and fill the entire space available to them:

    build_and_run_final

    You’ve now built a completely custom collection view layout – great work!

    Where to Go From Here?

    You can download the final project with all of the code from the tutorial.

    With less work than you probably thought, you’ve created your very own Pinterest-like custom layout!

    If you’re looking to learn more about custom layouts, consider the following resources:

    If you have any questions or comments on this tutorial, feel free to join the discussion below in the forums!

    The post UICollectionView Custom Layout Tutorial: Pinterest appeared first on Ray Wenderlich.

    Video Tutorial: Beginning Unity VR Part 3: Gamepad Support

    UICollectionView Custom Layout Tutorial: A Spinning Wheel

    $
    0
    0

    There are some really creative websites on the Internet, and a few weeks ago, I came across one such website called Form Follows Function, which is a collection of different kinds of interactive experiences. What really caught my attention was the site’s spinning navigation wheel, which contained posters that represented each kind of experience.

    Screenshot 2015-05-27 00.15.28

    This tutorial will show you how to use a UICollectionView custom layout to recreate this spinning navigation wheel. To get the most out of your time here, you’ll need to have basic knowledge of 2D transforms, collection views and custom layouts. If you’re unfamiliar with any of these topics then I recommend you check out the following before continuing:

    By the end of this tutorial, you’ll know how to:

    • Create your own collection view layout from scratch, without using UICollectionViewFlowLayout as your base class
    • Rotate views around a point outside their bounds

    And much, much more! Time to jump in.

    Getting Started

    First, download the starter project for this tutorial, open it in Xcode, and build and run. You’ll see a grid of cells, each representing a book from the raywenderlich.com store:

    Screenshot 2015-05-27 00.48.36

    The project’s setup is fairly straight forward. There’s CollectionViewController, and a custom collection view cell with an image view inside of it. The book covers are in a directory called Images, and CollectionViewController populates the collection view using the directory as its data source.

    Your task is to create a UICollectionViewLayout subclass to lay these cells out in a circular fashion.

    Theory

    Here’s a diagram of the wheel structure along with the cells. The yellow area is the iPhone’s screen, the blue rounded rectangles are the cells, and the dotted line is the circle you’ll place them around:

    Screenshot 2015-06-01 14.11.42

    You’ll need three main parameters to describe this arrangement:

    1. The radius of the circle (radius);
    2. The angle between each cell (anglePerItem);
    3. The angular position of cells.

    As you probably noticed, not all the cells fit within the screen’s bounds.

    Assume that the 0th cell has an angle of x degrees, then the 1st cell will have an angular position of x + anglePerItem, the second x + (2 * anglePerItem) and so on. This can be generalized for the nth item as:

    angle_for_i = x + (i * anglePerItem)

    Below, you’ll see a depiction of the angular coordinate system. An angle of 0 degrees refers to the center, while positive angles are shown towards the right and negative are towards the left. So a cell with an angle of 0 will lie in the center — completely vertical.

    Screenshot 2015-06-01 14.41.07

    Now that you’re clear on the underlying theories, you’re ready to start coding!

    Circular Collection View Layout

    Create a new Swift file with the iOS\Source\Cocoa Touch Class template. Name it CircularCollectionViewLayout, and make it a subclass of UICollectionViewLayout:

    Dialog

    Click Next, and then Create. This collection view layout subclass will contain all the positioning code.

    As this is a subclass of UICollectionViewLayout rather than UICollectionViewFlowLayout, you’ll have to handle all parts of the layout process yourself instead of piggybacking the parents implementation using calls to super.

    On that note, I find that flow layout is well suited for grids, but not for circular layouts.

    In CircularCollectionViewLayout, create properties for itemSize and radius:

    let itemSize = CGSize(width: 133, height: 173)
     
    var radius: CGFloat = 500 {
      didSet {
        invalidateLayout()
      }
    }

    When the radius changes, you recalculate everything, hence the call to invalidateLayout() inside didSet.

    Below the radius declaration, define anglePerItem:

    var anglePerItem: CGFloat {
      return atan(itemSize.width / radius)
    }

    anglePerItem can be any value you want, but this formula ensures that the cells aren’t spread too far apart.

    Next, implement collectionViewContentSize() to declare how big the content of your collection view should be:

    override func collectionViewContentSize() -> CGSize {
      return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0)) * itemSize.width,
          height: CGRectGetHeight(collectionView!.bounds))
    }

    The height will be the same as the collection view, but its width will be itemSize.width * numberOfItems.

    Now, open Main.storyboard, select Collection View in the document outline:

    Outline

    Open the Attributes Inspector and change Layout to Custom, and Class to CircularCollectionViewLayout:

    Attributes

    Build and run. Apart from a scrollable area, you won’t see anything, but that’s exactly what you want to see! It confirms that you’ve correctly told the collection view to use CircularCollectionViewLayout as its layout class.

    Screenshot 2015-06-01 03.07.31

    Custom Layout Attributes

    Along with a collection view layout subclass, you’ll also need to subclass UICollectionViewLayoutAttributes to store the angular position and anchorPoint.

    Add the following code to CircularCollectionViewLayout.swift, just above the CircularCollectionViewLayout class declaration:

    class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
      // 1
      var anchorPoint = CGPoint(x: 0.5, y: 0.5)
      var angle: CGFloat = 0 {
        // 2 
        didSet {
          zIndex = Int(angle * 1000000)
          transform = CGAffineTransformMakeRotation(angle)
        }
      }
      // 3
      override func copyWithZone(zone: NSZone) -> AnyObject {
        let copiedAttributes: CircularCollectionViewLayoutAttributes = 
            super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
        copiedAttributes.anchorPoint = self.anchorPoint
        copiedAttributes.angle = self.angle
        return copiedAttributes
      }
    }
    1. You need anchorPoint because the rotation happens around a point that isn’t the center.
    2. While setting angle, you internally set transform to be equal to a rotation of angle radians. You also want cells on the right to overlap the ones to their left, so you set zIndex to a function that increases in angle. Since angle is expressed in radians, you amplify its value by 1,000,000 to ensure that adjacent values don’t get rounded up to the same value of zIndex, which is an Int.
    3. This overrides copyWithZone(). Subclasses of UICollectionViewLayoutAttributes need to conform to the NSCopying protocol because the attribute’s objects can be copied internally when the collection view is performing a layout. You override this method to guarantee that both the anchorPoint and angle properties are set when the object is copied.

    Now, jump back to CircularCollectionViewLayout and implement layoutAttributesClass():

    override class func layoutAttributesClass() -> AnyClass {
      return CircularCollectionViewLayoutAttributes.self
    }

    This tells the collection view that you’ll be using CircularCollectionViewLayoutAttributes, and not the default UICollectionViewLayoutAttributes for your layout attributes.

    To hold layout attributes instances, create an array called attributesList below all other property declarations in CircularCollectionViewLayout:

    var attributesList = [CircularCollectionViewLayoutAttributes]()

    Preparing the Layout

    The first time the collection view appears on screen, the UICollectionViewLayout method prepareLayout() is called. This method is also called each time the layout is invalidated.

    This is one of the most crucial methods of the layout process, because it’s where you create and store layout attributes. Make it happen by adding the following to CircularCollectionViewLayout:

    override func prepareLayout() {
      super.prepareLayout()
     
      let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds) / 2.0)
      attributesList = (0..<collectionView!.numberOfItemsInSection(0)).map { (i) 
          -> CircularCollectionViewLayoutAttributes in
        // 1
        let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: i,
            inSection: 0))
        attributes.size = self.itemSize
        // 2
        attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
        // 3
        attributes.angle = self.anglePerItem*CGFloat(i)
        return attributes
      }
    }

    In short, you iterate over each item in the collection view and execute the closure. Keep reading for a line-by-line explanation:

    1. Create an instance of CircularCollectionViewLayoutAttributes for each index path, and then set its size.
    2. Position each item at the center of the screen.
    3. Rotate each item by the amount anglePerItem * i, in radians.
    Note: The method used here, map, is part of the Swift standard library and creates a new array with the results of the closure for each element in the range. You can find out more about the functional programming side of Swift here.

    To properly subclass UICollectionViewLayout you’ll also have to override the following methods, which return the layout attributes for the items in the given rect, and the item at the given index path respectively. The collection view will call these method numerous times throughout the layout process, as well as when the user scrolls the collection view, so it’s important that they’re efficient – hence why you create and cache the layout attributes in prepareLayout(). Add them below prepareLayout():

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
      return attributesList
    }
     
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) 
        -> UICollectionViewLayoutAttributes! {
      return attributesList[indexPath.row]
    }

    The first method simply returns the entire array of attributes, and the second method returns the attributes for the item at the given index path. This approach is OK for the purposes of this tutorial since you only have a small number of items, but usually you would want to iterate over the array and check whether the frame of the layout attributes intersects with the given rect, and only return those layout attributes whose frame does intersect. This would result in the collection view only drawing those items that should be on-screen, or which are about to come on screen.

    Build and run. You’ll see cells appear on screen, but rather than rotating around an external point, they rotate around themselves. It’s not quite the desired effect, but it is cool, don’t you think?

    Screenshot 2015-05-27 17.56.29

    Any guess as to why this is happening?

    Did Someone Say Anchor Point?

    Do you remember the discussion about the anchor point of the cell? You didn’t set it yet, hence the rotation is a touch crazy and not quite what you were looking to achieve.

    oh_yes_I_knew_that

    The anchor point is a property of CALayer around which all rotations and scaling transforms take place. The default value of this property is the center, as you saw in the last build and run.

    For the actual anchor point, the x value will remain 0.5, as you’ll observe in the diagram below. The y value, however, will be radius + (itemSize.height / 2), and since the anchor point is defined in the unit coordinate space, you’ll divide the result by itemSize.height.

    Screenshot 2015-06-01 16.22.12

    So jump back to prepareLayout(), and right below the definition of centerX define anchorPointY:

    let anchorPointY = ((itemSize.height / 2.0) + radius) / itemSize.height

    And inside the map(_:) closure, right before the return statement, add this line:

    attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)

    Next, open CircularCollectionViewCell.swift and override applyLayoutAttributes(_:) with the following:

    override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
      super.applyLayoutAttributes(layoutAttributes)
      let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
      self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
      self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
    }

    Here, you’re using the superclass implementation to apply the default properties like center and transform, but since anchorPoint is a custom property, you have to apply that manually. You also update center.y to the center of the layout circle to compensate for the change in anchorPoint.y.

    Build and run. You’ll see the cells are now laid out in a circle and when you scroll they…wait, what’s going on here? They’re just moving off-screen rather than rotating!?

    It’s going to be terribly difficult to find the right book! :]

    scrolling off

    Improving Scrolling

    The most challenging part of laying out the items is done, congratulations! :]

    ragecomic
    Now you just have to just play around with angle values to implement scrolling.

    Jump back to CircularCollectionViewLayout and add the following to the bottom of the class:

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
      return true
    }

    Returning true from this method tells the collection view to invalidate it’s layout as it scrolls, which in turn calls prepareLayout() where you can recalculate the cells’ layout with updated angular positions.

    angle is defined as the angular position of the 0th item. You’ll implement scrolling by converting contentOffset.x into a suitable value from angle.

    contentOffset.x goes from 0 to collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds) as you scroll. Call the extreme value of contentOffset.x as maxContentOffset. At 0, you want the 0th item at the center, and at the extreme, you want the last item at the center of the screen, which means the last item’s angular position will be zero.

    State of wheel at start (left) and at end (right)

    The state of your navigation wheel at the start (left) and the end (right)

    Consider the scenario on the right, and what would happen if you solve the following equation with angle_for_last_item = 0. You would get this:

    angle_for_last_item = angle_for_zero_item + (totalItems - 1) * anglePerItem
    0 = angle_for_zero_item + (totalItems - 1) * anglePerItem
    angle_for_zero_item = -(totalItems - 1) * anglePerItem

    Defining -(totalItems - 1) * anglePerItem as angleAtExtreme, you can write:

    contentOffset.x = 0, angle = 0
    contentOffset.x = maxContentOffset, angle = angleAtExtreme

    From here, it’s quite easy to interpolate angle for any value of contentOffset.x using the following formula:

    angle = -angleAtExtreme * contentOffset.x / maxContentOffset

    Keeping all this math in mind, add the following properties below the declaration for itemSize:

    var angleAtExtreme: CGFloat {
      return collectionView!.numberOfItemsInSection(0) > 0 ? 
        -CGFloat(collectionView!.numberOfItemsInSection(0) - 1) * anglePerItem : 0
    }
    var angle: CGFloat {
      return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize().width - 
        CGRectGetWidth(collectionView!.bounds))
    }

    Next, replace this line in prepareLayout():

    attributes.angle = (self.anglePerItem * CGFloat(i))

    with this one:

    attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))

    This adds the value of angle to each item, so that rather than being a constant, its angular position is a function of contentOffset.x.

    Build and run. Scroll across the screen and you’ll see that the items now rotate as you scroll. Much better!

    final scrolling

    Since you used the value of contentOffset.x to derive the value for angular position, you get features like rubber banding, extremes checking and deceleration for free — without having to write any additional code. Bet you feel smarter already!

    Bonus Material: Optimizations

    You’ve successfully recreated the spinning navigation wheel, so give yourself a well deserved pat on the back! You could put your feet up on the desk and end the session here, but why would you stop there when there’s room for some scroll-smoothing optimization?

    In prepareLayout() you create instances of CircularCollectionViewLayoutAttributes for every item, but not all of them end up on the screen at once. For these off-screen items, you can completely skip calculations and just not create layout attributes at all.

    But there is a bit of a challenge here: you need to determine which item is inside the screen and which is off-screen. In the diagram below, any item that lies outside the range of (-θ, θ) will be off-screen.

    Screenshot 2015-06-01 17.46.48

    For instance, to calculate θ in triangle ABC, you’d do this:

    tanθ = (collectionView.width / 2) / (radius + (itemSize.height / 2) - (collectionView.height / 2))

    Add the following code to prepareLayout(), just below the declaration of anchorPointY:

    // 1 
    let theta = atan2(CGRectGetWidth(collectionView!.bounds) / 2.0, 
        radius + (itemSize.height / 2.0) - (CGRectGetHeight(collectionView!.bounds) / 2.0))
    // 2
    var startIndex = 0
    var endIndex = collectionView!.numberOfItemsInSection(0) - 1 
    // 3
    if (angle < -theta) {
      startIndex = Int(floor((-theta - angle) / anglePerItem))
    }
    // 4
    endIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
    // 5
    if (endIndex < startIndex) {
      endIndex = 0
      startIndex = 0
    }

    What are you doing here?:

    1. You find theta by using the tan inverse function;
    2. You initialize startIndex and endIndex to 0 and the last item index respectively;
    3. If the angular position of the 0th item is less than -theta, then it lies outside the screen. In that case, the first item on the screen will be the difference between and angle divided by anglePerItem;
    4. Similarly, the last element on the screen will be the difference between θ and angle divided by anglePerItem, and min serves as an additional check to ensure endIndex doesn’t go beyond the total number of items;
    5. Lastly, you add a safety check to make the range 0...0 if endIndex is less than startIndex. This edge case occurs when you scroll with a very high velocity and all the cells go completely off-screen.

    Here’s a diagram to explain the calculations above visually:

    Click for higher resolution image

    Click for higher resolution image

    Now that you know which items are on-screen and which aren’t, you need to update the range used to calculate the layout attributes in prepareLayout(). Find this line:

    attributesList = (0..<collectionView!.numberOfItemsInSection(0)).map { (i) 
        -> CircularCollectionViewLayoutAttributes in

    and replace it with this one:

    attributesList = (startIndex...endIndex).map { (i) 
        -> CircularCollectionViewLayoutAttributes in

    Now build and run. You’ll see no visual difference because all the changes affect off-screen items, but you should see fewer cells when you open Xcode’s builtin view hierarchy debugger.

    And since you’re creating fewer objects, you should also see a improvement in the performance.

    Screenshot 2015-05-27 23.33.45

    Where To Go From Here

    You can download the completed project here.

    Screenshot 2015-06-01 03.03.33

    Congratulations, you’ve successfully used a UICollectionView custom layout to implement a spinning navigation wheel.

    You’ve learned a number of things in this tutorial, including how to rotate views, change their anchor point, create your own custom collection view layout from scratch, and how to make it all look pretty.

    To keep the learning party going, try playing around with values likes radius and anglePerItem in the layout to see how they affect the final circular arrangement. While this tutorial focuses on 2D transforms, you can create interesting effects by employing similar techniques to apply rotations in 3D space with transform3D.

    You can also implement snapping behavior by overriding targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:) in CircularCollectionViewLayout.

    Think you’re up to the task? Go for it. If you get stuck, open the spoiler below.

    Solution Inside: Snapping behavior SelectShow>

    If you have questions, comments or would like to show off how you took the concepts in this tutorial to the next level, please join the discussion below!

    The post UICollectionView Custom Layout Tutorial: A Spinning Wheel appeared first on Ray Wenderlich.

    Video Tutorial: Beginning Core Data: Series Introduction

    $
    0
    0

    Video Resources

    This series uses the latest Xcode 7 and iOS 9 beta. Be sure you have the latest SDK, and use Xcode 7 to open all the sample projects!

    Download lecture slides

    Other frameworks:

    If you’re interested in learning more about these third-party frameworks, check out some of our other posts:

    The post Video Tutorial: Beginning Core Data: Series Introduction appeared first on Ray Wenderlich.

    Viewing all 4370 articles
    Browse latest View live


    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>