Leveraging Real-World Physics
The built-in gravity dynamic animator consists of a downward force. You can adjust the force’s vector to point gravity in other directions, but it’s a static system. You can, however, integrate the gravity behavior with Core Motion to produce a much more satisfying effect. Apple’s Core Motion framework enables your apps to receive motion-based data from device hardware, including the onboard accelerometer and gyroscope. The framework converts motion data into a form of input that your device can use to coordinate application changes with the way your user’s device is held and moved over time.
Listing 6-4 builds a motion manager singleton. It uses Core Motion to listen for accelerometer updates, and when it receives them, it calculates a working vector and posts notifications with that information. You may be curious about that extra 0.5 added to the y component; it produces a more natural vector for holding a device in your hand.
Listing 6-4 Broadcasting Motion Updates
#define VALUE(struct) ({ __typeof__(struct) __struct = struct; [NSValue valueWithBytes:&__struct objCType:@encode(__typeof__(__struct))]; }) NSString *const MotionManagerUpdate = @”MotionManagerUpdate”; NSString *const MotionVectorKey = @”MotionVectorKey”; static MotionManager *sharedInstance = nil; @interface MotionManager () @property (nonatomic, strong) CMMotionManager *motionManager; @end @implementation MotionManager + (instancetype) sharedInstance { if (!sharedInstance) sharedInstance = [[self alloc] init]; return sharedInstance; } - (void) shutDownMotionManager { NSLog(@”Shutting down motion manager”); [_motionManager stopAccelerometerUpdates]; _motionManager = nil; } - (void) establishMotionManager { if (_motionManager) [self shutDownMotionManager]; // Establish the motion manager NSLog(@”Establishing motion manager”); _motionManager = [[CMMotionManager alloc] init]; } - (void) startMotionUpdates { if (!_motionManager) [self establishMotionManager]; if (_motionManager.accelerometerAvailable) [_motionManager startAccelerometerUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:^(CMAccelerometerData *data, NSError *error) { CGVector vector = CGVectorMake(data.acceleration.x, - (data.acceleration.y + 0.5)); NSDictionary *dict = @{MotionVectorKey:VALUE(vector)}; [[NSNotificationCenter defaultCenter] postNotificationName:MotionManagerUpdate object:self userInfo:dict]; }]; } @end
Connecting a Gravity Behavior to Device Acceleration
On the other end of things, create an observer for motion updates. The following snippet builds a gravity behavior and updates its gravityDirection property whenever the physical device moves:
// Build device gravity behavior _deviceGravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[]]; // Add observer __weak typeof(self) weakSelf = self; id observer = [[NSNotificationCenter defaultCenter] addObserverForName:MotionManagerUpdate object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { __strong typeof(self) strongSelf = weakSelf; // Retrieve vector NSDictionary *dict = note.userInfo; NSValue *value = dict[MotionVectorKey]; CGVector vector; [value getValue:&vector]; // Set gravity direction to that vector strongSelf.deviceGravityBehavior.gravityDirection = vector; }]; [_observers addObject:observer];
As the gravityDirection property updates, any child items (none are yet added in this code) respond to the new force, moving in the appropriate direction.
Creating Boundaries
One of the biggest annoyances about gravity is that it never stops. When you apply a gravity behavior to a view, it will accelerate off the screen and keep going on essentially forever. Bye-bye, view. To avoid this, add a boundary. The UICollisionBehavior has a built-in solution for enclosures. Enable its translatesReferenceBoundsIntoBoundary property, and it sets the animator’s reference view as a default boundary for its items:
_boundaryBehavior = [[UICollisionBehavior alloc] initWithItems:@[]]; _boundaryBehavior.translatesReferenceBoundsIntoBoundary = YES;
When building behaviors like this, it’s important to spot-check your key steps. Remember that animators own behaviors, and behaviors own items, which are typically views. Don’t forget to add items to each behavior that affects them. For this example of device-based gravity, add views to both the gravity behavior and the boundary behavior. Also, make sure to add the behaviors to the animator. Always make sure your views fall fully within the collision boundaries before adding a behavior to the animator. Views that cross the boundary or lie outside the boundary will not respond properly to the “keep items within the reference bounds” rule.
Collision behaviors also enable views to bounce off each other. By default, any view added to a collision behavior will participate not only in view-to-boundary collisions but also in view-to-view collisions. If for any reason you don’t want this to happen, you can update the behavior’s collisionMode property to exclude item-to-item collisions:
_boundaryBehavior = [[UICollisionBehavior alloc] initWithItems:@[]]; _boundaryBehavior.translatesReferenceBoundsIntoBoundary = YES; _boundaryBehavior.collisionMode = UICollisionBehaviorModeBoundaries;
Enhancing View Dynamics
Dynamic item behaviors customize view traits—making them springier or duller, heavier or lighter, smoother or stickier, and so forth. Unlike the other built-in behaviors, dynamic item behaviors focus less on external forces and more on individual view properties. For example, say you have views that you want to add bounce to. Create a dynamic item behavior and adjust its elasticity property:
_elasticityBehavior = [[UIDynamicItemBehavior alloc] initWithItems:items]; _elasticityBehavior.elasticity = 0.8; // Higher values are more elastic [_animator addBehavior:_elasticityBehavior];
Dynamic item properties include the following:
- Rotation (allowsRotation)—This property allows or disallows view rotation as the view participates in the dynamic system. When it is enabled (the default), views may rotate as they collide with other items.
- Angular resistance (angularResistance)—Angular resistance creates a damping effect on rotation. As the value of this property rises from 0 to 1, views stop tumbling more quickly.
- Resistance (resistance)—Also ranging from 0 to 1, the linear resistance property is analogous to angular resistance. Instead of damping rotation, it limits linear velocity. You can think of this as a natural viscosity in the view’s “atmosphere,” where 0 is close to operating in a vacuum, and 1 is like moving through thick syrup.
- Density (density)—An item’s density property controls its virtual mass. Any dynamic behavior that uses mass as a factor (such as collisions and friction) responds to the current value of this property, which defaults to 1. Because items have density, a view that’s twice the size of another along each dimension will contribute four times the effective mass when set to the same density or equal mass when set to a quarter of the density.
- Elasticity (elasticity)—Ranging from 0 to 1, this property establishes how elastic a view’s collisions will be. At 0, collisions are lifeless, with no bounce at all. A setting of 1 creates completely elastic collisions with wildly bouncy items.
- Friction (friction)—The friction property creates linear resistance, producing a kind of “stickiness” for when items slide across each other. As the friction setting rises from 0 (friction-free) to 1 (the strongest possible friction), views tend to disperse energy on contact and connect more strongly to each other and to boundaries.