iOS 7 UIKit Dynamics: Gravity and Collisions

UIKit Dynamics is an API added in iOS 7. Dynamics animations are meant to take UIKit controls and mimic real world analogs such as an object falling due to gravity. In this post, we’ll examine how to make views fall and collide with Dynamic Behaviors.

To demonstrate Dynamics, we’ll use this little guy, hereby known as Block Man.

UIKit Dynamics

Block Man is just a combination of strategically placed UIView’s to look familiar as it changes. Poor Block Man is going to get thrown all over your iPhone screen, all in the name of learning. In theory, you could replace Block Man’s views with any other views or controls: UISlider’s, UIToolbar’s, UIButton’s, etc. For this tutorial, we’ll keep it easy and stick with UIView’s.

To get started with the screenshot above, create a new Single View application and copy the following code into the View Controller (replacing MGOViewController with the name of your View Controller). If you prefer, there is a link near the bottom of the tutorial to download the UIKit Dynamics Xcode project.

#import "MGOViewController.h"
 
#define kHeadInitialPosition CGRectMake(135, 100, 50, 50)
#define kTorsoInitialPosition CGRectMake(155, 150, 10, 100)
#define kUpperLegInitialPosition CGRectMake(140, 250, 40, 10)
#define kLeftLegInitialPosition CGRectMake(130, 250, 10, 40)
#define kRightLegInitialPosition CGRectMake(180, 250, 10, 40)
#define kLeftUpperArmInitialPosition CGRectMake(115, 170, 40, 10)
#define kRightUpperArmInitialPosition CGRectMake(165, 170, 40, 10)
#define kLeftLowerArmInitialPosition CGRectMake(105, 170, 10, 40)
#define kRightLowerArmInitialPosition CGRectMake(205, 170, 10, 40)
 
@interface MGOViewController ()
 
@property (nonatomic, strong) NSArray *bodyParts;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, strong) UIGravityBehavior *gravity;
 
@property (nonatomic, strong) UIView *head;
@property (nonatomic, strong) UIView *torso;
@property (nonatomic, strong) UIView *upperLegs;
@property (nonatomic, strong) UIView *leftLeg;
@property (nonatomic, strong) UIView *rightLeg;
@property (nonatomic, strong) UIView *upperLeftArm;
@property (nonatomic, strong) UIView *upperRightArm;
@property (nonatomic, strong) UIView *lowerLeftArm;
@property (nonatomic, strong) UIView *lowerRightArm;
 
@end
 
@implementation MGOViewController
 
- (void)viewDidLoad
{
  [super viewDidLoad];
 
  [self createBlockMan];
}
 
- (void) createBlockMan
{
  self.head = [[UIView alloc] initWithFrame:kHeadInitialPosition];
  [self.view addSubview:self.head];
 
  UIView *leftEye = [[UIView alloc] initWithFrame:CGRectMake(15, 15, 5, 5)];
  leftEye.backgroundColor = [UIColor whiteColor];
  [self.head addSubview:leftEye];
 
  UIView *rightEye = [[UIView alloc] initWithFrame:CGRectMake(30, 15, 5, 5)];
  rightEye.backgroundColor = [UIColor whiteColor];
  [self.head addSubview:rightEye];
 
  UIView *mouth = [[UIView alloc] initWithFrame:CGRectMake(15, 35, 20, 3)];
  mouth.backgroundColor = [UIColor whiteColor];
  [self.head addSubview:mouth];
 
  self.torso = [[UIView alloc] initWithFrame:kTorsoInitialPosition];
  [self.view addSubview:self.torso];
 
  self.upperLegs = [[UIView alloc] initWithFrame:kUpperLegInitialPosition];
  [self.view addSubview:self.upperLegs];
 
  self.leftLeg = [[UIView alloc] initWithFrame:kLeftLegInitialPosition];
  [self.view addSubview:self.leftLeg];
 
  self.rightLeg = [[UIView alloc] initWithFrame:kRightLegInitialPosition];
  [self.view addSubview:self.rightLeg];
 
  self.upperLeftArm = [[UIView alloc] 
    initWithFrame:kLeftUpperArmInitialPosition];
  [self.view addSubview:self.upperLeftArm];
 
  self.upperRightArm = [[UIView alloc] 
    initWithFrame:kRightUpperArmInitialPosition];
  [self.view addSubview:self.upperRightArm];
 
  self.lowerLeftArm = [[UIView alloc] 
    initWithFrame:kLeftLowerArmInitialPosition];
  [self.view addSubview:self.lowerLeftArm];
 
  self.lowerRightArm = [[UIView alloc] 
    initWithFrame:kRightLowerArmInitialPosition];
  [self.view addSubview:self.lowerRightArm];
 
  self.bodyParts = @[self.head,
                     self.torso,
                     self.upperLegs,
                     self.leftLeg,
                     self.rightLeg,
                     self.upperLeftArm,
                     self.upperRightArm,
                     self.lowerLeftArm,
                     self.lowerRightArm];
 
  [self.bodyParts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, 
    BOOL *stop) {
      [(UIView *)obj setBackgroundColor:[UIColor blackColor]];
  }];
}
 
@end

It’s a lot of code, but all it’s doing is setting up a bunch of views to look like Block Man. Now that Block Man is on the screen, let’s make him disappear – with gravity! Modify the viewDidLoad method and add the methods below:

- (void)viewDidLoad
{
  [super viewDidLoad];
 
  [self createBlockMan];
 
  UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(tapRecognized:)];
  [self.view addGestureRecognizer:tap];
}
 
- (void) tapRecognized: (UITapGestureRecognizer *) sender
{
  [self.animator addBehavior:self.gravity];
}
 
- (UIGravityBehavior *) gravity
{
  if (!_gravity)
    _gravity = [[UIGravityBehavior alloc] initWithItems:self.bodyParts];
 
  return _gravity;
}
 
- (UIDynamicAnimator *) animator
{
  if (!_animator)
    _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
 
  return _animator;
}

You may have noticed in the first block of code the inclusion of the gravity and animator properties. The animator property is a UIDynamicAnimator object. This is really the glue that holds all dynamic animations together.

Dynamic Behaviors, such as our UIGravityBehavior gravity property, are added to the Dynamic Animator. A UIGravityBehavior object is exactly what it sounds like; a behavior that mimics gravity on a set of objects.

In this case, you’ll notice the bodyParts array is passed in the UIGravityBehavior constructor, meaning these objects will have gravity applied to them. The leftEye, rightEye, and mouth UIView’s are purposefully left out of this array, as we want their position to remain fixed within the head view.

UIKit Dynamics

It’s worth pointing out that the UIGravityBehavior and other UIDynamicBehaviors don’t simply act on UIView’s. Any object that implements the UIDynamicItem protocol can be used in a dynamic behavior.

The UIDynamicItem protocol only contains three properties: center, bounds, and transform. It’s not a coincidence that these properties already exist on UIView, and it is common to use UIView’s in dynamic behaviors. However, this protocol opens the door to all kinds of custom dynamic implementations.

Now that Block Man falls off the bottom of the screen, let’s have a little more fun. While in the real world gravity always accelerates in the same direction, with Dynamics we can make gravity go in any direction we want. Change the tapRecognized method as follows:

- (void) tapRecognized: (UITapGestureRecognizer *) sender
{
  CGPoint tapPoint = [sender locationInView:self.view];
  CGVector direction = CGVectorMake((tapPoint.x - self.view.center.x) 
    / (self.view.frame.size.width / 2),
    (tapPoint.y - self.view.center.y) 
    / (self.view.frame.size.height / 2));
  [self changeGravityDirection:direction];
}
 
- (void) changeGravityDirection: (CGVector) direction
{
  self.gravity.gravityDirection = direction;
 
  if (!self.gravity.dynamicAnimator)
    [self.animator addBehavior:self.gravity];
}

The tapRecognized method will now look at where the user taps on the screen, compare it to the center of the view, and adjust gravity’s direction. The UIGravityBehavior’s direction property controls both where gravity takes the objects (up, down, left, right, etc) and how quickly it accelerates the objects to get there.

The math in the tapRecognized method takes into account both slope from the center and distance from the center. Therefore, the further you tap from the center of the view, the faster Block Man will accelerate towards the edge of the screen. In addition, Block Man will “fall” in whatever direction you tap, when compared to the center.

UIKit Dynamics

Now that we can make Block Man go any direction we want, the next obvious step is to actually keep him on the screen. After all, we want to see what happens when he lands, don’t we?

For this, we’ll add a UICollisionBehavior. This is again a very well named class; UICollisionBehavior’s govern what happens when the bounds of two UIDynamicItems meet because of another dynamic behavior, such as gravity. Collision behaviors can be created anywhere on the screen, represented by objects or by complex Bezier paths.

For the purposes of this tutorial, we will keep it simply and make collision behaviors between all the body parts and the sides of the view, such that none can overlap each other. Modify viewDidLoad as follows:

- (void)viewDidLoad
{
  [super viewDidLoad];
 
  [self createBlockMan];
  [self createWalls];
 
  UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
    initWithTarget:self action:@selector(tapRecognized:)];
  [self.view addGestureRecognizer:tap];
}
 
- (void) createWalls
{
  UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc]
    initWithItems:self.bodyParts];
  collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
  [self.animator addBehavior:collisionBehavior];
}

This is pretty simple. Again, we are passing in the bodyParts property so the collision behavior knows to stop all body parts from overlapping. We are also turning on the translatesReferenceBoundsIntoBoundary property. This simply puts up collision boundaries on the edges of our main view so no body parts can cross. We could do this by adding extra dynamic items or collision boundaries, but this is fewer lines of code and much more convenient.

Animation3

Now we can create some real Block Man carnage. Note that you can still change the direction of gravity with any tap of the screen, so once Block Man has gone into a crumpled heap on one side of the screen, you can change gravity and toss him to the other side.

UIKit Dynamics

Unfortunately for Block Man, there’s more. There’s a pretty robust physics engine running behind the scenes of all these dynamic animations. Because of this, we’re also able to change properties of dynamic items that will change the effects of other dynamic behaviors.

In the real world, density, friction, elasticity, and other properties all play a part in how objects fall and collide. In the UIKit Dynamic world, we can modify these properties through UIDynamicItemBehavior’s. There are too many properties on UIDynamicItemBehavior to dive into all of them in this post, but for Block Man, we’re going to make his head bounce around just a little bit more than the rest of his body. Modify the createWalls method as follows:

- (void) createWalls
{
  UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc]
    initWithItems:self.bodyParts];
  collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
  [self.animator addBehavior:collisionBehavior];
 
  UIDynamicItemBehavior *headBounce = [[UIDynamicItemBehavior alloc]
    initWithItems:@[self.head]];
  headBounce.elasticity = 0.75;
  [self.animator addBehavior:headBounce];
}

The new code is pretty self explanatory. We’re creating another new behavior, this one governing properties of a UIDynamicItem. We set the behavior’s elasticity property to .75, which on a scale from 0.0 to 1.0 is noticeably more elastic than the default of .5. Add the behavior to the animator and Block Man’s head bounces much more pronounced on the view.

UIKit Dynamics

We’ll add one more bit of code in this tutorial so you can play with Block Man endlessly, another tap gesture, this time specifically to Block Man’s head, which will reset him to his original position. Modify the viewDidLoad method like so:

- (void)viewDidLoad
{
  [super viewDidLoad];
 
  [self createBlockMan];
  [self createWalls];
 
  UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(tapRecognized:)];
  [self.view addGestureRecognizer:tap];
 
  UITapGestureRecognizer *headTap = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(headTap:)];
  [self.head addGestureRecognizer:headTap];
 
}
 
- (void) headTap: (UIGestureRecognizer *) sender
{
  [self.animator removeAllBehaviors];
 
  [UIView animateWithDuration:2.0
                        delay:0.0
       usingSpringWithDamping:.4
        initialSpringVelocity:20
                      options:UIViewAnimationOptionCurveLinear
             animations:^{                 
                   [self.bodyParts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
                       [(UIView *)obj setTransform:CGAffineTransformIdentity];
                   }];
 
                   self.head.frame = kHeadInitialPosition;
                   self.torso.frame = kTorsoInitialPosition;
                   self.upperLegs.frame = kUpperLegInitialPosition;
                   self.leftLeg.frame = kLeftLegInitialPosition;
                   self.rightLeg.frame = kRightLegInitialPosition;
                   self.upperRightArm.frame = kRightUpperArmInitialPosition;
                   self.upperLeftArm.frame = kLeftUpperArmInitialPosition;
                   self.lowerRightArm.frame = kRightLowerArmInitialPosition;
                   self.lowerLeftArm.frame = kLeftLowerArmInitialPosition;
               }
               completion:^(BOOL finished) {
                   [self createWalls];
               }];
}

This headTap method is chock full of important stuff. First, notice that before we do anything we remove all behaviors from the animator. This is important; we’re about to move these objects back to their original locations, in the opposite direction of gravity, when they might cross each other as they go. We don’t want dynamic behaviors getting in the way of our Block Man rebuild.

When our animation is complete, we’ll call createWalls again to recreate most of our behaviors; we don’t add the gravity behavior back in yet as that will be added when the user taps the screen. Second, notice that inside the animation block we are not only reseting all the object’s frames to their initial position. We are also resetting the transform to the identity transform. The identity transform is essentially no transform; because of the direction of gravity and the way objects collide, they might have been rotated or had other transforms applied that need to be reset.

Finally, there is one more gem in this method. Consider it my reward to you for making it to the end of the post. The animateWithDuration method has two new parameters in it: spring dampening and initial velocity. This new animate method will make changes along the path of a spring, reverberating back and forth until it reaches it’s resting state.

Springs can be represented by attachment behaviors, which are another kind of dynamic behavior we will talk about in the next post. Until then, know that you have this powerful method at your disposal that adds a whole bunch of dynamics behind the scenes to make your animation dynamic. If you tap on Block Man’s head now, you’ll see him reconstruct in his original location. However, as that happens, you’ll notice the objects that compose him first over shoot their mark, then come back slower and over shoot again, then finally rest at their desired location.

UIKit Dynamics

Additional Reading


Mike Oliver has been a mobile junkie every since his first “Hello World” on a spinach-screened Blackberry. Lately, he works primarily with iOS, but he’s always looking for new ways to push the envelope from your pocket. Mike is currently the Lead iOS Engineer at RunKeeper, where he tries to make the world a healthier place one app at a time.