UIKit Dynamics Tutorial: Getting Started
Learn how to make your user interfaces in iOS feel realistic with this UIKit Dynamics tutorial, updated for Swift! By James Frost.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UIKit Dynamics Tutorial: Getting Started
25 mins
Update note: This tutorial is an abbreviated version of a chapter from iOS 7 by Tutorials by Colin Eberhardt. It was updated for iOS 8 and Swift by James Frost, and checked against Xcode 6 beta 7.
The design goals of iOS encourage you to create digital interfaces that react to touch, gestures, and changes in orientation as if they were physical objects far beyond a simple collection of pixels. The end result gives the user a deeper connection with the interface than is possible through skin-deep skeuomorphism.
This sounds like a daunting task, as it is much easier to make a digital interface look real, than it is to make it feel real. However, you have some nifty new tools on your side: UIKit Dynamics and Motion Effects.
- UIKit Dynamics is a full physics engine integrated into UIKit. It allows you to create interfaces that feel real by adding behaviors such as gravity, attachments (springs) and forces. You define the physical traits that you would like your interface elements to adopt, and the dynamics engine takes care of the rest.
- Motion Effects allows you to create cool parallax effects like you see when you tilt the iOS 7 home screen. Basically you can harness the data supplied by the phone’s accelerometer in order to create interfaces that react to changes in phone orientation.
When used together, motion and dynamics form a powerhouse of user experience tools that make your digital interfaces come to life. Your users will connect with your app at a deeper level by seeing it respond to their actions in a natural, dynamic way.
Note: At the time of writing this tutorial, our understanding is we cannot post screenshots of iOS 8 since it is still in beta. All the screenshots here are from iOS 7, which should look very close to what things will look like in iOS 8.
Getting started
UIKit dynamics can be a lot of fun; the best way to start learning about them is to jump in feet-first with some small examples.
Open Xcode, select File / New / Project … then select iOS Application / Single View Application and name your project DynamicsDemo. Once the project has been created, open ViewController.swift and add the following code to the end of viewDidLoad
:
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
The above code simply adds a square UIView
to the interface.
Build and run your app, and you’ll see a lonely square sitting on your screen, as shown below:
If you’re running your app on a physical device, try tilting your phone, turning it upside-down, or even shaking it. What happens? Nothing? That’s right — everything is working as designed. When you add a view to your interface you expect it to remain firmly stuck in place as defined by its frame — until you add some dynamic realism to your interface!
Adding gravity
Still working in ViewController.swift, add the following properties above viewDidLoad
:
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
These properties are implicitly-unwrapped optionals (as denoted by the !
after the type name). These properties must be optional because you won’t be initializing them in our class’s init
method. You can use implicitly-unwrapped optionals because we know that these properties won’t be nil
after you’ve initialized them. This prevents you from having to manually unwrap their values with the !
operator each time.
Add the following to the end of viewDidLoad
:
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
I’ll explain this in a moment. For now, build and run your application. You should see your square slowly start to accelerate in a downward motion until it drops off the bottom of the screen, as so:
In the code you just added, there are a couple of dynamics classes at play here:
-
UIDynamicAnimator
is the UIKit physics engine. This class keeps track of the various behaviors that you add to the engine, such as gravity, and provides the overall context. When you create an instance of an animator, you pass in a reference view that the animator uses to define its coordinate system. -
UIGravityBehavior
models the behavior of gravity and exerts forces on one or more items, allowing you to model physical interactions. When you create an instance of a behavior, you associate it with a set of items — typically views. This way you can select which items are influenced by the behavior, in this case which items the gravitational forces affect.
Most behaviors have a number of configuration properties; for example, the gravity behavior allows you to change its angle and magnitude. Try modifying these properties to make your objects fall up, sideways, or diagonally with varying rates of acceleration.
NOTE: A quick word on units: in the physical world, gravity (g) is expressed in meters per second squared and is approximately equal to 9.8 m/s2. Using Newton’s second law, you can compute how far an object will fall under gravity’s influence with the following formula:
distance = 0.5 × g × time2
In UIKit Dynamics, the formula is the same but the units are different. Rather than meters, you work with units of thousands of pixels per second squared. Using Newton’s second law you can still work out exactly where your view will be at any time based on the gravity components you supply.
Do you really need to know all this? Not really; all you really need to know is that a bigger value for g means things will fall faster, but it never hurts to understand the math underneath.
Setting boundaries
Although you can’t see it, the square continues to fall even after it disappears off the bottom of your screen. In order to keep it within the bounds of the screen you need to define a boundary.
Add another property in ViewController.swift:
var collision: UICollisionBehavior!
Add these lines to the bottom of viewDidLoad
:
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
The above code creates a collision behavior, which defines one or more boundaries with which the associated items interact.
Rather than explicitly adding boundary co-ordinates, the above code sets the translatesReferenceBoundsIntoBoundary
property to true
. This causes the boundary to use the bounds of the reference view supplied to the UIDynamicAnimator
.
Build and run; you’ll see the square collide with the bottom of the screen, bounce a little, then come to rest, as so:
That’s some pretty impressive behavior, especially when you consider just how little code you’ve added at this point.
Handling collisions
Next up you’ll add an immovable barrier that the falling square will collide and interact with.
Insert the following code to viewDidLoad
just after the lines that add the square to the view:
let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.redColor()
view.addSubview(barrier)
Build and run your app; you’ll see a red “barrier” extending halfway across the screen. However, it turns out the barrier isn’t that effective as the square falls straight through the barrier:
That’s not quite the effect you were looking for, but it does provide an important reminder: dynamics only affect views that have been associated with behaviors.
Time for a quick diagram:
UIDynamicAnimator
is associated with a reference view that provides the coordinate system. You then add one or more behaviors that exert forces on the items they are associated with. Most behaviors can be associated with multiple items, and each item can be associated with multiple behaviors. The above diagram shows the current behaviors and their associations within your app.
Neither of the behaviors in your current code is “aware” of the barrier, so as far as the underling dynamics engine is concerned, the barrier doesn’t even exist.
Making objects respond to collisions
To make the square collide with the barrier, find the line that initializes the collision behavior and replace it with the following:
collision = UICollisionBehavior(items: [square, barrier])
The collision object needs to know about every view it should interact with; therefore adding the barrier to the list of items allows the collision object to act upon the barrier as well.
Build and run your app; the two objects collide and interact, as shown in the following screenshot:
The collision behavior forms a “boundary” around each item that it’s associated with; this changes them from objects that can pass through each other into something more solid.
Updating the earlier diagram, you can see that the collision behavior is now associated with both views:
However, there’s still something not quite right with the interaction between the two objects. The barrier is supposed to be immovable, but when the two objects collide in your current configuration the barrier is knocked out of place and starts spinning towards the bottom of the screen.
Even more oddly, the barrier bounces off the bottom of the screen and doesn’t quite settle down like the square – this makes sense because the gravity behavior doesn’t interact with the barrier. This also explains why the barrier doesn’t move until the square collides with it.
Looks like you need a different approach to the problem. Since the barrier view is immovable, there isn’t any need to for the dynamics engine to be aware of its existence. But how will the collision be detected?
Invisible boundaries and collisions
Change the collision behavior initialization back to its original form so that it’s only aware of the square:
collision = UICollisionBehavior(items: [square])
Immediately after this line, add the following:
// add a boundary that has the same frame as the barrier
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame))
The above code adds an invisible boundary that has the same frame as the barrier view. The red barrier remains visible to the user but not to the dynamics engine, while the boundary is visible to the dynamics engine but not the user. As the square falls, it appears to interact with the barrier, but it actually hits the immovable boundary instead.
Build and run your app to see this in action, as below:
The square now bounces off the boundary, spins a little, and then continues its journey towards the bottom of the screen where it comes to rest.
By now the power of UIKit Dynamics is becoming rather clear: you can accomplish quite a lot with only a few lines of code. There’s a lot going on under the hood; the next section shows you some of the details of how the dynamic engine interacts with the objects in your app.
Behind the scenes of collisions
Each dynamic behavior has an action property where you supply a block to be executed with every step of the animation. Add the following code to viewDidLoad
:
collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))")
}
The above code logs the center and transform properties for the falling square. Build and run your app, and you’ll see these log messages in the Xcode console window.
For the first ~400 milliseconds you should see log messages like the following:
[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}
Here you can see that the dynamics engine is changing the center of the square — that is, its frame— in each animation step.
As soon as the square hits the barrier, it starts to spin, which results in log messages like the following:
[0.99797821, 0.063557133, -0.063557133, 0.99797821, 0, 0] {152, 247}
[0.99192101, 0.12685727, -0.12685727, 0.99192101, 0, 0] {154, 244}
[0.97873402, 0.20513339, -0.20513339, 0.97873402, 0, 0] {157, 241}
Here you can see that the dynamics engine is using a combination of a transform and a frame offset to position the view according to the underlying physics model.
While the exact values that dynamics applies to these properties are probably of little interest, it’s important to know that they are being applied. As a result, if you programmatically change the frame or transform properties of your object, you can expect that these values will be overwritten. This means that you can’t use a transform to scale your object while it is under the control of dynamics.
The method signatures for the dynamic behaviors use the term items rather than views. The only requirement to apply dynamic behavior to an object is that it adopts the UIDynamicItem
protocol, as so:
protocol UIDynamicItem : NSObjectProtocol {
var center: CGPoint { get set }
var bounds: CGRect { get }
var transform: CGAffineTransform { get set }
}
The UIDynamicItem
protocol gives dynamics read and write access to the center and transform properties, allowing it to move the items based on its internal computations. It also has read access to bounds, which it uses to determine the size of the item. This allows it to create collision boundaries around the perimeter of the item as well as compute the item’s mass when forces are applied.
This protocol means that dynamics is not tightly coupled to UIView
; indeed there is another UIKit class that isn’t a view but still adopts this protocol: UICollectionViewLayoutAttributes
. This allows dynamics to animate items within collection views.
Collision notifications
So far you have added a few views and behaviors then let dynamics take over. In this next step you will look at how to receive notifications when items collide.
Still in ViewController.swift, adopt the UICollisionBehaviorDelegate
protocol by updating the class declaration:
class ViewController: UIViewController, UICollisionBehaviorDelegate {
In viewDidLoad
, set the view controller as the delegate just after the collision object is initialized, as follows:
collision.collisionDelegate = self
Next, add an implementation for one of the collision behavior delegate methods to the class:
func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
println("Boundary contact occurred - \(identifier)")
}
This delegate method is called when a collision occurs. It prints out a log message to the console. In order to avoid cluttering up your console log with lots of messages, feel free to remove the collision.action
logging you added in the previous section.
Build and run; your objects will interact, and you’ll see the following entries in your console:
Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
From the log messages above you can see that the square collides twice with the boundary identifier barrier; this is the invisible boundary you added earlier. The (null) identifier refers to the outer reference view boundary.
These log messages can be fascinating reading (seriously!), but it would be much more fun to provide a visual indication when the item bounces.
Below the line that sends message to the log, add the following:
let collidingView = item as UIView
collidingView.backgroundColor = UIColor.yellowColor()
UIView.animateWithDuration(0.3) {
collidingView.backgroundColor = UIColor.grayColor()
}
The above code changes the background color of the colliding item to yellow, and then fades it back to gray again.
Build and run to see this effect in action:
The square will flash yellow each time it hits a boundary.
So far UIKit Dynamics has automatically set the physical properties of your items (such as mass and elasticity) by calculating them based on your item’s bounds. Next up you’ll see how you can control these physical properties yourself by using the UIDynamicItemBehavior
class.
Configuring item properties
Within viewDidLoad
, add the following to the end of the method:
let itemBehaviour = UIDynamicItemBehavior(items: [square])
itemBehaviour.elasticity = 0.6
animator.addBehavior(itemBehaviour)
The above code creates an item behavior, associates it with the square, and then adds the behavior object to the animator. The elasticity property controls the bounciness of the item; a value of 1.0 represents a completely elastic collision; that is, where no energy or velocity is lost in a collision. You’ve set the elasticity of your square to 0.6, which means that the square will lose velocity with each bounce.
Build and run your app, and you’ll notice that the square now behaves in a bouncier manner, as below:
Note: If you are wondering how I produced the above image with trails that show the previous positions of the square, it was actually very easy! I simply added a block to the action property of one of the behaviors, and every third time the block code was executed, added a new square to the view using the current center and transform from the square. You can see my solution in the spoiler section below.
[spoiler title=”Trails”]
var updateCount = 0
collision.action = {
if (updateCount % 3 == 0) {
let outline = UIView(frame: square.bounds)
outline.transform = square.transform
outline.center = square.center
outline.alpha = 0.5
outline.backgroundColor = UIColor.clearColor()
outline.layer.borderColor = square.layer.presentationLayer().backgroundColor
outline.layer.borderWidth = 1.0
self.view.addSubview(outline)
}
++updateCount
}
[/spoiler]
In the above code you only changed the item’s elasticity; however, the item’s behavior class has a number of other properties that can be manipulated in code. They are as follows:
- elasticity – determines how ‘elastic’ collisions will be, i.e. how bouncy or ‘rubbery’ the item behaves in collisions.
- friction – determines the amount of resistance to movement when sliding along a surface.
- density – when combined with size, this will give the overall mass of an item. The greater the mass, the harder it is to accelerate or decelerate an object.
- resistance – determines the amount of resistance to any linear movement. This is in contrast to friction, which only applies to sliding movements.
- angularResistance – determines the amount of resistance to any rotational movement.
- allowsRotation – this is an interesting one that doesn’t model any real-world physics property. With this property set to NO the object will not rotate at all, regardless of any rotational forces that occur.
Adding behaviors dynamically
In its current state, your app sets up all of the behaviors of the system, then lets dynamics handle the physics of the system until all items come to rest. In this next step, you’ll see how behaviors can be added and removed dynamically.
Open ViewController.swift and add the following property, above viewDidLoad
:
var firstContact = false
Add the following code to the end of the collision delegate method collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:)
if (!firstContact) {
firstContact = true
let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
collision.addItem(square)
gravity.addItem(square)
let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square)
animator.addBehavior(attach)
}
The above code detects the initial contact between the barrier and the square, creates a second square and adds it to the collision and gravity behaviors. In addition, you set up an attachment behavior to create the effect of attaching a pair of objects with a virtual spring.
Build and run your app; you should see a new square appear when the original square hits the barrier, as shown below:
While there appears to be a connection between the two squares, you can’t actually see the connection as a line or spring since nothing has been drawn on the screen to represent it.
User Interaction
As you’ve just seen, you can dynamically add and remove behaviours when your physics system is already in motion. In this final section, you’ll add another type of dynamics behaviour, UISnapBehavior
, whenever the user taps the screen. A UISnapBehavior
makes an object jump to a specified position with a bouncy spring-like animation.
Remove the code that you added in the last section: both the firstContact
property and the if
statement in collisionBehavior()
. It’s easier to see the effect of the UISnapBehavior
with only one square on screen.
Add two properties above viewDidLoad
:
var square: UIView!
var snap: UISnapBehavior!
This will keep track of your square view, so that you can access it from elsewhere in the view controller. You’ll be using the snap
object next.
In viewDidLoad
, remove the let
keyword from the declaration of the square, so that it uses the new property instead of a local variable:
square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
Finally, add an implementation for touchesEnded
, to create and add a new snap behavior whenever the user touches the screen:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
if (snap != nil) {
animator.removeBehavior(snap)
}
let touch = touches.anyObject() as UITouch
snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view))
animator.addBehavior(snap)
}
This code is quite straightforward. First, it checks if there’s an existing snap behavior and removes it. Then creates a new snap behavior which snaps the square to the location of the user’s touch, and adds it to the animator.
Build and run your app. Try tapping around; the square should zoom into place wherever you touch!
Where To Go From Here?
At this point you should have a solid understanding of the core concepts of UIKit Dynamics. You can download the final DynamicsDemo project from this tutorial for further study.
UIKit Dynamics brings the power of a physics engine to your iOS apps. With subtle bounces and springs and gravity, you can bring life to your apps and make them feel more real and immersive for your users.
If you’re interested in learning more about UIKit Dynamics, check out our book iOS 7 By Tutorials. The book takes what you’ve learned so far and goes a step further, showing you how to apply UIKit Dynamics in an real world scenario:
The user can pull up on a recipe to take a peek at it, and when they release the recipe, it will either drop back into the stack, or dock to the top of the screen. The end result is an application with a real-world physical feel.
I hope you enjoyed this UIKit Dynamics tutorial – we think it’s pretty cool and look forward to seeing the creative ways you use it in your apps. If you have any questions or comments, please join the forum discussion below!