UIKit Dynamics Tutorial

来源:互联网 发布:java存储数据的方法 编辑:程序博客网 时间:2024/06/06 00:02

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

UIKit Dynamics Tutorial

Note from Ray: This is an abbreviated version of a chapter from iOS 7 by Tutorials that we are releasing as part of the iOS 7 Feast. We hope you enjoy!

You have probably come to realize that iOS 7 is something of a paradox; while you’re being encouraged to do away with real-world metaphors and skeuomorphism, Apple encourages you at the same time to create user interfaces that feel real.

What does this mean in practice? The design goals of iOS 7 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.

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 DynamicsPlayground. Once the project has been created, open ViewController.mand add the following code to the end of viewDidLoad:

UIView* square = [[UIView alloc] initWithFrame:                                CGRectMake(100, 100, 100, 100)];square.backgroundColor = [UIColor grayColor];[self.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:

LonelySquare

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.m, add the following instance variables:

UIDynamicAnimator* _animator;UIGravityBehavior* _gravity;

Add the following to the end of viewDidLoad:

_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];_gravity = [[UIGravityBehavior alloc] initWithItems:@[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:

FallingSquare

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 gmeans 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 instance variable in ViewController.m:

UICollisionBehavior* _collision;

Add these lines to the bottom of viewDidLoad:

_collision = [[UICollisionBehavior alloc]                                      initWithItems:@[square]];_collision.translatesReferenceBoundsIntoBoundary = YES;[_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 YES. 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:

SquareAtRest

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:

UIView* barrier = [[UIView alloc] initWithFrame:CGRectMake(0, 300, 130, 20)];barrier.backgroundColor = [UIColor redColor];[self.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:

BadBarrier

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:

DynamicClasses

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 alloc] initWithItems:@[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:

GoodBarrier

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:

DynamicClasses2

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 alloc] initWithItems:@[square]];

Next, add a boundary as follows:

// add a boundary that coincides with the top edgeCGPoint rightEdge = CGPointMake(barrier.frame.origin.x +                                barrier.frame.size.width, barrier.frame.origin.y);[_collision addBoundaryWithIdentifier:@"barrier"                            fromPoint:barrier.frame.origin                              toPoint:rightEdge];

The above code adds an invisible boundary that coincides with the top edge of 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 line instead.

Build and run your app to see this in action, as below:

BestBarrier

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 =  ^{    NSLog(@"%@, %@",           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:

2013-07-26 08:21:58.698 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 236}2013-07-26 08:21:58.715 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 243}2013-07-26 08:21:58.732 DynamicsPlayground[17719:a0b] [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:

2013-07-26 08:21:59.182 DynamicsPlayground[17719:a0b] [0.10679234, 0.99428135, -0.99428135, 0.10679234, 0, 0], {198, 325}2013-07-26 08:21:59.198 DynamicsPlayground[17719:a0b] [0.051373702, 0.99867952, -0.99867952, 0.051373702, 0, 0], {199, 331}2013-07-26 08:21:59.215 DynamicsPlayground[17719:a0b] [-0.0040036771, 0.99999201, -0.99999201, -0.0040036771, 0, 0], {201, 338}

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 <NSObject> @property (nonatomic, readwrite) CGPoint center;@property (nonatomic, readonly) CGRect bounds;@property (nonatomic, readwrite) CGAffineTransform transform; @end

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 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.

Open ViewController.m and adopt the UICollisionBehaviorDelegate protocol:

@interface ViewController () <UICollisionBehaviorDelegate> @end

Still in viewDidLoad, set the view controller as the delegate just after the collision behavior has been instantiated, as follows:

_collision.collisionDelegate = self;

Next, add an implementation for one of the collision behavior delegate methods:

- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id<UIDynamicItem>)item             withBoundaryIdentifier:(id<NSCopying>)identifier atPoint:(CGPoint)p {    NSLog(@"Boundary contact occurred - %@", identifier);}

This delegate method is fired off when a collision occurs and 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:

2013-07-26 08:44:37.473 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier2013-07-26 08:44:37.689 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier2013-07-26 08:44:38.256 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)2013-07-26 08:44:38.372 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)2013-07-26 08:44:38.455 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)2013-07-26 08:44:38.489 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)2013-07-26 08:44:38.540 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

From the log messages 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 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:

UIView* view = (UIView*)item;view.backgroundColor = [UIColor yellowColor];[UIView animateWithDuration:0.3 animations:^{    view.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:

YellowCollision

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 or 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:

UIDynamicItemBehavior* itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[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:

PrettyBounce

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 fifth time the block code was executed, added a new square to the view using the current center and transform from the square.

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.m and add the following instance variable:

BOOL _firstContact;

Add the following code to the end of the collision delegate method collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint:

if (!_firstContact){    _firstContact = YES;     UIView* square = [[UIView alloc] initWithFrame:CGRectMake(30, 0, 100, 100)];    square.backgroundColor = [UIColor grayColor];    [self.view addSubview:square][_collision addItem:square];    [_gravity addItem:square];     UIAttachmentBehavior* attach = [[UIAttachmentBehavior alloc] initWithItem:view                                                               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:

Attachment

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.

Where To Go From Here?

At this point you should have a solid understanding of the core concepts of UIKit Dynamics.

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:

SandwichFlowDynamics

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!

The full sourcecode for the Dynamics Playground you have built in this tutorial is available on github, with a commit for each ‘build and run’ step.


http://www.raywenderlich.com/50197/uikit-dynamics-tutorial

原创粉丝点击