Apple Watch Home Screen UI Animation

iNVASIVECODE
[cocoa gurus]
Published in
9 min readJul 8, 2015

--

An iOS Developer Tutorial

Apple Watch is here and everybody is waiting to play with this nice new toy. In the last two keynotes, Apple showed us Apple Watch and the amazing UI gives iOS developers new innovative design options that will make iOS apps feel intuitively alive. Two weeks ago, we had the opportunity to test our app on an Apple Watch during a lab in Cupertino. This tutorial is especially cool because it examines the technological depth of the new Apple Watch UI animation.

Here’s a small portion of the Watch ad that shows the device home screen. For sure, you notice how the icons animate on the new, gorgeous Apple Watch UI.

This is just a 2 second video, but, with the right eye, it is long enough to analyze and discover how the UI works. So, in this tutorial, I will show you how to build this cool animation on your iPhone. Since I do not own a device yet, it took some close observation to understand the dynamics of the icons.

Collection view

I could build the Apple Watch home screen animation in many different ways, but I decided to go for collection views (instance of a UICollectionView class), where each round icon of the watch UI is represented by a collection view cell (instance of UICollectionViewCell). For this case, I think collection views provide all the tools I need for this experiment.

The project

I am going to build this example using Swift. So, launch Xcode and create a new iOS project. Name it WatchUI and choose Swift as main language.

Open the Storyboard and add a collection view to the scene. Add also auto layout so that the collection view always keeps a size of 390x312 pixels and it is always centered vertically and horizontally in the scene view.

Control-click the collection view and connect the datasource outlet to the view controller.

Let’s add now a new class to the project and subclass UICollectionViewCell. Name this class CollectionViewCell.

As you saw in the video ad, the watch icons are circular, so we need to work with the cell’s layer and modify its corner radius. To do so, I override the init method of this new class and I change the cornerRadius and the backgroundColor property of the cell content view layer.

override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.layer.cornerRadius = frame.width / 2.0
self.contentView.layer.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.9, alpha: 0.8).CGColor
}

Notice that I am using self in front of the contentView property. I always use self when I need to refer to a class property. I do this for readability. I don’t at all like the fact that in Swift you can refer to a property with no need of the self keyword. When you have tons of lines of code, that small self keyword can help you understand the code quicker.

Now, go to the ViewController.swift file and add an outlet for the collection view:

@IBOutlet weak var collectionView: UICollectionView!

Connect the outlet to the collection view in the Storyboard.

In the viewDidLoad(), let’s register the cell to the collection view and associate the custom layout to the same collection view:

override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.collectionViewLayout = CollectionViewLayout()
self.collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "MyCell")
}

Let’s also add the collection view datasource protocol to the class:

class ViewController: UIViewController, UICollectionViewDataSource {

Finally, let’s add the two datasource methods to populate the collection view:

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cellCount
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as CollectionViewCell
return cell
}

The cellCount can be defined before the class

let cellCount = ROWS * COLS

I am going to define ROWS and COLS in the next class.

Layout magic

Initially, I thought of building a collection view using the flow layout, but then I decided to go for something more flexible. So, I decided to use a full custom collection view layout and subclass UICollectionViewLayout.

So, let’s add to our project a subclass of UICollectionViewLayout and name it CollectionViewLayout. Here is where we will do most of our work to replicate the Apple Watch home screen animation. However, before adding any line of code, let’s try to understand how the Apple Watch home screen works.

If you look at the Apple Watch movie ad multiple times, you will notice that the UI looks as if each icon is moving on a sphere. However, if you look at it carefully and slow down the video, you will notice that when an icon reaches the screen’s edge it scales down faster than the icons closer to the center. Therefore, my conclusion is that the curve on which the icons move is not a sphere.

Let’s play with some geometry. Every Mac is shipped with a fantastic (and almost unknown) tool called Grapher. You can find it in your Utilities folder. Launch this app, choose the 3D Graph tab and click Choose.

You can use this app to build 2D and 3D graphs. I use it a lot when modeling mathematical functions. It’s very handy. In the top command line, you can write mathematical equations and they will be rendered directly in the canvas. So, in the command line, insert the following equations one by one:

a = 312
b = 390
c = 20
z = -c((x/a)^2 + (y/a)^2) + 1.0

Then, go to the menu View -> Frame Limits… and change the values as shown in the following picture.

You can download here the Grapher document I am using for this experiment.

Now, let’s take a look at the mathematical equation I am using here. It is an elliptic paraboloid, a quadratic surface represented by the following mathematical equation:

I chose this 3D function, because I think this is the mathematical model that describes the scale of each icon on the Apple Watch home screen. Imagine the paraboloid coming out of the Apple Watch screen, with its top point placed in the center of the screen. The following picture highlights what I mean:

I had to slightly modify this equation to make it work for my case. If you compare it with the equation I used in Grapher, you will notice a couple of differences. First, I multiplied the second part of the equation by -1.0. I did this in order to rotate the paraboloid upside down, so that its tip points towards the positive z-axis. Then, I added 1.0 to translate the entire paraboloid 1 unit towards the positive z-axis. In a moment, it will be clear why I chose to do this.

The paraboloid provides the scale factor I will use for each collection view cell rendered on the screen. Watch again at the TV ad. The icon that comes to the center (by the way, the light blue icon with a white lantern is Findery (an app developed by my friend John Fox and his team) seems to reach its maximum scale. So, when a cell is located in the center of the screen, the scale value should be 1.0. After the translation along the z-axis, the tip of the paraboloid has value z = 1.0. Instead, as the cell moves away from the center, the scale value goes down to zero following the curvature of the paraboloid. Now, you understand why I added 1.0 for the entire 3D curve.

Let’s code all this. First, I need to build a special grid of cells. In the CollectionViewLayout class, I create 2 constants representing the number of rows and columns of the grid:

let COLS = 20
let ROWS = 20

In the collection view layout class, let’s add the following properties:

1- This represents the space between two adjacent cells:

let interimSpace: CGFloat = 0.0

2- Each cell has a diameter of 80 points

let itemSize: CGFloat = 80

3- A computed property representing the center of the animation:

var center: CGPoint {
return CGPoint(x: (self.cViewSize.width) / 2.0,
y: (self.cViewSize.height) / 2.0)
}

4- A computed property that holds the total number of cells:

var cellCount: Int {
return COLS*ROWS
}

5- A computed property holding the size of the collection view:

var cViewSize: CGSize {
return self.collectionView!.frame.size
}

6- A computed property holding the value of the paraboloid parameter a:

var a: CGFloat {
return 2.5 * self.cViewSize.width
}

7- A computed property holding the value of the paraboloid parameter b:

var b: CGFloat {
return 2.5 * self.cViewSize.height
}

8- A stored property holding the value of the paraboloid parameter c:

let c: CGFloat = 20

Since I want to change the layout of the collection view every time the user scrolls it, I need to invalidate the collection view layout at every bounds change. To do so, I add the following method to the CollectionViewLayout class:

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

The collection view content size is obtained by the size of each cell plus the space between two consecutive adjacent cells multiplied by the number of cells in the horizontal and vertical direction:

override func collectionViewContentSize() -> CGSize {
return CGSizeMake(self.itemSize * CGFloat(COLS) + self.cViewSize.width,
self.itemSize * CGFloat(ROWS) + self.cViewSize.height)
}

The layout attributes for each element in the rect of the collection view:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var attributes = [AnyObject]()
for i in 0 ..< cellCount {
let indexPath = NSIndexPath(forItem: i, inSection: 0) attributes.append(self.layoutAttributesForItemAtIndexPath(indexPath))
}
return attributes
}

Here, I am simply getting the layout attributes for each cell at a given indexPath calling the layoutAttributesForItemAtIndexPath(_☺ method. I also implement this method that will be called for each cell in the collection view:

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes!
{
var attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
let oIndex = indexPath.item % COLS // 1
let vIndex = (indexPath.item - oIndex) / COLS // 2
var x = CGFloat(oIndex) * self.itemSize // 3
var y = CGFloat(vIndex) * self.itemSize // 4
if vIndex % 2 != 0 { // 5
x += self.itemSize / 2.0
}
attributes.center = CGPoint(x: x, y: y) // 6 let offset = self.collectionView!.contentOffset // 7
x -= (self.center.x + CGFloat(offset.x)) // 8
y -= (self.center.y + CGFloat(offset.y)) // 9
x = -x*x/(a*a) // 10
y = -y*y/(b*b) // 11
var z = c * (x+y) + 1.0 // 12
z = z < 0.0 ? 0.0 : z // 13
attributes.transform = CGAffineTransformMakeScale(z, z) // 14
attributes.size = CGSize(width: self.itemSize, height: self.itemSize) // 15
return attributes
}

In the above chuck of code, lines 1 and 2 are used to compute the row and column position of a cell at indexPath. Then, lines 3 and 4 are used to compute the x and y coordinates of the center of each cell. The if statement in line 5 is used to shift the odd rows of the grid to the right of an amount equal to half cell size. Line 6 assigns the computed x and y value to attribute center. From line 7 until line 13, I build the paraboloid equation to compute z that I am using in line 14 as the scale factor of the cell. Line 15 applies the size attribute to each cell.

We are ready to go. If you run the example and try to play with the value of a, b and c, you can obtain different effects.

Conclusions

I hope you enjoyed re-creating the Apple Watch UI in Swift. I’m looking forward to using WatchKit in our iOS training. As developers, we seek to create real world interactions. But behind each simple, intuitive gesture and every visually stunning UI exists endless complexity. I enjoy confronting this complexity and finding simplicity’s source.

Keep Coding.

Geppy

Geppy Parziale (@geppyp) is cofounder of InvasiveCode (@invasivecode). He has developed iOS applications and taught iOS development since 2008. He worked at Apple as iOS and OS X Engineer in the Core Recognition team. He has developed several iOS and OS X apps and frameworks for Apple, and many of his development projects are top-grossing iOS apps that are featured in the App Store.

--

--

iNVASIVECODE
[cocoa gurus]

iOS consulting and training. Expertise include: machine vision, pattern-recognition, biometrics, and loT.