Charles Brian Quinn
UIKit Dynamics is one of the more fun parts of iOS 7, giving us user interface elements that mimic real physical objects. They can bump into each other, move and spin, fall with gravity and bounce around on invisible springs and strings. I decided to build an app that plays Pong using only UIKit in order to see how it all worked.
To set up, I created a project with an empty view and programmatically added two views, one for the ball and one for the paddle. I made the ball square and the paddle a wide rectangle. I also created an animator for the system. The animator is just an instance of UIAnimator and runs the physics engine for the system.
To start the ball moving, it needs a push. So I created a push behavior for the ball and made it instantaneous, instead of continuous, so the ball would coast.
// Start ball off with a push self.pusher = [[UIPushBehavior alloc] initWithItems:@[self.ballView] mode:UIPushBehaviorModeInstantaneous]; self.pusher.pushDirection = CGVectorMake(0.5, 1.0); self.pusher.active = YES; // Because push is instantaneous, it will only happen once [self.animator addBehavior:self.pusher];
This is a bit inverted from how we might think of it in English, or in object-oriented programming in general. You would think that something like
[ball push] would work. But by making the various laws of physics into objects you can easily modify them; if they were just methods on the view objects, then you would have an explosion of methods like
[ball slowlySlideInclineUntilYouHitSomething]. So it is a little neater to add the behaviors to the animator instead of the items they act on.
Here is what it looked like:
The push behavior imparts some momentum to the ball, and it moves. A good start, but it flies right through the paddle and off the view! By default there is no collision of views, so I added a collision behavior.
// Add collisions self.collider = [[UICollisionBehavior alloc] initWithItems:@[self.ballView, self.paddleView]]; self.collider.collisionDelegate = self; self.collider.collisionMode = UICollisionBehaviorModeEverything; self.collider.translatesReferenceBoundsIntoBoundary = YES; [self.animator addBehavior:self.collider];
This will make the paddle and ball collide with each other and the walls. The walls are the sides of the reference view that we set up with the animator: the view containing the ball and paddle. If you wanted to just collide with the walls or each other, you could change the collisionMode property of the behavior.
Running the app, I was surprised to see this:
The ball and paddle collide and they spin! This makes sense and is kind of fun, but some sort of rectangular snooker is not what I was going for here.
Keeping the views from spinning requires setting some of their dynamic properties. Once again there’s a bit of indirection involved; you have to set up a set of dynamic item behaviors and assign the views to them. This will turn off rotation:
// Remove rotation self.ballDynamicProperties = [[UIDynamicItemBehavior alloc] initWithItems:@[self.ballView]]; self.ballDynamicProperties.allowsRotation = NO; [self.animator addBehavior:self.ballDynamicProperties]; self.paddleDynamicProperties = [[UIDynamicItemBehavior alloc] initWithItems:@[self.paddleView]]; self.paddleDynamicProperties.allowsRotation = NO; [self.animator addBehavior:self.paddleDynamicProperties];
With this the paddle doesn’t spin, but it does move around a bit. This confounded me a bit; how could I make the ball bounce off the paddle without having the paddle move?
What I did was make the paddle really, really heavy. I thought of making it a ping pong ball bouncing off a rock instead of a tennis ball hitting a soccer ball. I changed the paddle’s density, which is exposed in the dynamic item behavior:
// Heavy paddle self.paddleDynamicProperties.density = 1000.0f;
The default density is 1.0, so this made the paddle 1000 times heavier. The ball will bounce without the paddle moving. Although I do not like using a magic number like 1000, when I tried CGFLOAT_MAX the animator threw an exception.
The bounce is still not ideal; the ball slows down after a bit and the collision uses up some energy. Dynamic behavior properties once again fix this. Make the ball perfectly elastic, which means all of the kinetic energy in a collision will get transferred.
// Better collisions, no friction self.ballDynamicProperties.elasticity = 1.0; self.ballDynamicProperties.friction = 0.0; self.ballDynamicProperties.resistance = 0.0;
This also sets the friction of the ball 0, which keeps it from getting stuck on the edge, and the resistance to 0, which keeps it from slowing down as it flies through the view. Now it is starting to looks like pong!
You may notice that there are a lot of objects being created to do this. UIKit Dynamics is a composable system, meaning you figure out what rules you want to apply to each object and add them to it. Want that view to fall? Add a gravity behavior. Don’t want it to disappear off the bottom of the screen? Add a collision with the screen boundary.
Composable systems can be nice because they give you lots of ways to combine effects and generate new ones, but the cost is having to learn lots of classes. To get this far I needed push, collision and dynamic behavior classes.
Finally, the user needed to be able to move the paddle. This revealed another interesting aspect of UIKit Dynamics, which is that it wants to control the properties of the animated objects. When I set the center point of the paddle, it would just flicker and then jump back to where it was previously (if it did anything at all). This is because the animator sets the center and transform property of the view every tick of the animation.
To modify the item properties yourself, you need to tell the animator that you’ve changed the item properties using
updateItemUsingCurrentState:. Doing this lets the user tap and move the paddle, which I did in a tap gesture handler:
self.paddleView.center = [gr locationInView:self.view]; [self.animator updateItemUsingCurrentState:self.paddleView];
I could also have used a
UISnapBehavior to move the paddle; this could have the paddle fly to the new position instead of teleporting. But a side effect of this is if the paddle hits the ball while it is moving, it adds more energy to the ball, making it move too quickly and erratically. I would need to adjust the dynamic item properties to keep the ball at a sane speed. This can involve hierarchical behaviors and how the animator traverses the behavior graph; important stuff, but something for another blog post!
If you want to check out the code, look at our iOS7Demos project on Github and play with WrongPong. We cover UIKit Dynamics and many other new iOS 7 topics in our Advanced iOS class. I’ll also be speaking about it this Friday at CocoaConf Columbus and again in October at CocoaConf Boston.
I’ll leave you with a little animation of the app after I messed with the snap behavior and put some styling of the views. Ready for the app store, don’t you think?
Charles Brian Quinn