This workshop will be retired on May 31, 2020.
UIKit Dynamics30:37 with Josh Timonen
In this workshop, we will build a swipe-out tray using Apple’s UIKitDynamics framework. This allows us to add simulated physics to standard UI elements, such as UIViews, to build something that behaves and feels more like real-world object.
Different physics behaviors covered in this workshop:
- A gravity behavior
- A collision behavior for our edges
- An attachment behavior for our pan gestures
- A push behavior for our close button
[MUSIC] 0:00 iOS application development for iPhone and 0:04 iPad can offer many exciting opportunities for creative and engaging user interfaces. 0:06 Touch screen interaction is a great environment for 0:12 making things that feel real. 0:14 Apple's UIKit Dynamics framework allows us to 0:16 add simulated physics to standard UI elements such as UI views. 0:19 In this workshop, we're going to build a swipe-out try 0:23 that uses some of these physics tools to build something that behaves and 0:26 feels more like a real world object. 0:29 You could build a UI element like this without physics 0:32 by attaching a view to a pan gesture recognizer for example. 0:35 But, by adding some basic physics we can 0:39 breathe life into these interactive elements and 0:41 give our apps that extra polish that helps set them apart from the competition. 0:44 So let's get started. 0:48 First, let's create a new single-view project in Xcode. 0:49 We'll call it DynamicTrayDemo. 0:53 In the Info.plist, we'll modify supported interface orientations to 1:08 also allow Portrait (top home button). 1:12 Now, we'll be able to rotate to all orientations. 1:16 In the ViewController.m file, under viewDidLoad, 1:19 we'll create a UI image view with UIImageView *imageView 1:24 equals UIImageView alloc initWithImage UIImage:imageNamed@"mabel.jpg". 1:29 We'll also drag this image into the project. 1:37 This will just give us a full screen image background. 1:41 We'll set 1:44 translatesAutoResizingMaskIntoConstraints to NO. 1:44 Set the content mode to UIViewContentModeScaleAspectFill. 1:49 And then add it as a subview of self.view. 1:54 Next, we'll add some basic constraints to that view, 2:08 pinning it to the edges of its super view. 2:11 Once we've written the horizontal constraints, we'll just copy and 2:40 paste the line changing H to V for the vertical constraints. 2:47 If we run the app, we'll see the image view as expected and 2:52 rotations resize the image appropriately using aspect fill. 2:56 We actually need to add supportedInterfaceOrientations to our view 3:18 controller, and return UIInterfaceOrientationMaskAll in order to 3:22 actually to get all the rotations. 3:26 If we re-run the app, we'll see that we now have all the rotations. 3:38 Great. 3:46 Now, let's create a method called setupTrayView. 3:49 We'll call it after our imageView code in viewDidLoad. 3:53 We'll also create a property for this tray up in the interface. 4:07 This will be a UIVisualEffectView, so we can give it the blurred, 4:11 frosted glass effect. 4:14 In our setupTrayView method, we first need to create the blur effect. 4:19 UIBlurEffect *blurEffect 4:26 equals [UIBlurEffect 4:32 effectWithStyle:UIBlurEffectStyleExtraLig- 4:36 ht]. 4:45 Now, we'll create the visual effect view. 4:47 So, self.trayView = [UIVisualEffectView 4:52 alloc]initwithEffect:blurEffect. 4:58 Then, we'll set translatesAutoresizingMaskIntoConstraints 5:03 to know and add it as a subview of self.view. 5:10 Now let's set up the constraints for tray view. 5:15 Instead of pinning the view to the super view's edges, we'll set its width and 5:18 height equal to self.view. 5:23 So, self.view addConstraint:[NSLayoutConstraint 5:53 constraintWithItem:self.trayView AttributeWidth relatedBy 5:57 Equal toItem:self.view AttributeWidth multiplier:1.0 constant:0. 6:03 For the height constraint, we'll just copy and paste the same line. 6:11 Changing width to height. 6:14 So again, we're just setting the trayView's width to self.view's width and 6:22 trayView's height to self.view's height. 6:27 We'll also pin trayView's top to self.view's top. 6:30 Again, we'll just copy and paste the line and change Height in this case to Top. 6:37 For the left constraint, we'll want to add a property for it up in our interface. 6:48 So property (nonatomic, 7:04 strong) NSLayoutConstraint *trayLeftEdgeConstraint. 7:05 Now, down below the other tray constraints, we'll create the left 7:10 constraint, setting the tray's left edge equal to self.view's left edge. 7:14 But with the constant set to self.view.frame.size.width. 7:18 On the next line, we'll add this constraint to self.view. 7:47 Below this, we'll add a UILabel to the tray view, just so 7:55 it has some basic content. 7:59 UILabel *trayLabel equals UILabel new. 8:08 We'll set its text to, good morning, sunshine. 8:12 Mabel the Frenchie. 8:15 We'll set its text to Good Morning, Sunshine, MabelTheFrenchie. 8:29 And use \n for some line breaks. 8:33 We'll set the numberOfLines to 0, meaning it can be as many lines as it needs to be. 8:36 We'll set the font size to 24. 8:41 TranslatesAutoresizingMaskIntoConstraints to NO. 8:50 Then, add it as a subview of trayView. 8:54 Now we'll set this labels constraint basically pinning its edges to the super 9:02 view with 30 points of padding horizontally and 9:06 100 points of padding vertically. 9:09 Again, to do the vertical constraints we'll just copy the horizontal line, 9:43 changing H to V, and updating the padding from 30 to 100. 9:48 At the end of this method, we'll need to call self.view layout if needed to ensure 9:56 that the view is laid out before we start applying our UIKit Dynamics. 10:00 Next, we're going to setup some gesture recognizers. 10:09 We'll create a method named setupGestureRecognizers and 10:12 call it at the end of viewDidLoad. 10:15 We'll want to create a UI screen edge pan gesture recognizer which will 10:18 allow the user to pan from the right edge to expose the tray view. 10:22 It will call a method called pan, which we'll create in a minute. 10:27 So UIScreenEdgePanGestureRecognizer edgePan equals 10:46 UIScreenEdgePanGestureRecognizer alloc initWithTarget self action selector pan. 10:51 Next, we set the recognizer's edges property to UIRectEdgeRight. 10:58 This specifies that we want the gesture to only be recognized on the right edge of 11:06 the screen. 11:10 Then we add this recognizer to self.view. 11:11 Next, we'll create a second standard UIPanGestureRecognizer which will be 11:18 added to the TrayView. 11:23 This will be used to grab and dismiss the TrayView. 11:24 It's going to call 11:31 the same pan method. 11:36 This one gets added to self.trayView. 11:42 We'll set up the pan method shortly. 11:51 Now let's create our UIDynamicAnimator object. 11:53 The UIDynamicAnimator is the central object of our physics code. 11:58 We initialize it with a reference view, 12:02 which will contain all of our physics-based elements. 12:05 We create a single instance of it and then add or remove behavior objects to it, 12:08 such as collision behaviors, gravity behaviors, attachment, or push behaviors. 12:12 Each behavior is initialized with a view or array of views that it affects. 12:18 So let's first add a UIDynamicAnimator as a property in our interface. 12:23 Then, in viewDidLoad, we'll create it. 12:39 We write self.animator equals UIDynamicAnimator alloc 12:56 initWithReferenceView self.view. 13:00 We set the reference view to self.view, 13:03 which will be the root view of our physics environment. 13:06 Next, we'll add UIDynamicBehaviors to this animator. 13:10 Let's create a method called setupBehaviors and call it in viewDidLoad, 13:14 right under where we create our animator. 13:18 Inside setupBehaviors, the first behavior we'll create is a UICollisionBehavior. 13:24 This will create the outer walls of our physics environment. 13:29 This init method accepts an array of view objects that will be affected by 13:33 this behavior. 13:37 We'll only pass in our TrayView. 13:38 We'll write UICollisionBehavior edgeCollisionBehavior 13:41 equals UICollisionBehavior alloc initWithItems. 13:46 And then an array, which only contains self.trayView. 13:52 We're going to have the TrayView fill the screen, minus 100 points on the left edge. 13:57 So, you'll still be able to see the ImageView underneath the tray 14:03 in the 100 pixels on the left. 14:06 We'll use this gutter width value in a couple places shortly. 14:09 If you just want to turn the boundary of your reference 14:21 system into the physical walls, 14:26 which in our case would be self.view, you can just call 14:29 setTranslatesReferenceBoundsIntoBoundary YES. 14:34 But in our case, we want to use setTranslatesReferenceBoundsIntoBoundaryW- 14:39 ithInsets, which allows us to modify our edges. 14:44 Our top is 0, left is the gutter width, bottom is 0, and 15:05 right is the width of the screen, but a negative value. 15:10 This extra space to the right is where the tray will sit while not on screen. 15:14 Finally, we add this behavior to the animator. 15:19 Next, up in the interface, let's create a property for our gravity behavior. 15:29 So property nonatomic, strong UIGravityBehavior, and 15:45 we'll call it gravity. 15:49 Below the collision behavior code, we'll initialize our gravity behavior, 15:51 again passing in our TrayView as the only view for the items array. 15:55 On the next line, we'll add the gravity behavior to our animator. 16:08 Now, let's create a new method called updateGravityIsLeft. 16:19 It will accept a Boolean to tell us whether or not the gravity should be left. 16:34 This is where we'll actually set the gravity values. 16:38 The method accepts a Boolean named isLeft, 16:41 which identifies whether gravity is pulling towards the left. 16:43 If no, then gravity pulls to the right. 16:47 We need to create an angle in radians identifying gravity's direction. 16:50 We'll create a ternary operator here. 16:54 If left, we use PI. 16:57 If right, we use 0. 16:58 So CGFloat angle equals isLeft, question mark, either M_Pi or 0. 17:01 On the next line, we'll set the angle and magnitude on our gravity behavior. 17:08 We'll use magnitude of 1, which is intended to feel like Earth's gravity. 17:12 So we'll be changing the direction of our simulated gravity 17:17 to give the bounce at each extreme of the tray's movement. 17:20 Let's also create a new Boolean property in the interface named gravityIsLeft. 17:23 Now, at the end of setupBehaviors, 17:33 let's call our updateGravityIsLeft method, passing in this Boolean. 17:35 Next, let's set up our pan method that's wired up to our gesture recognizers. 17:44 The first thing we need to do is get the point of the panGesture in our self.view. 17:58 So CGPoint currentPoint equals recognizer locationInView self.view. 18:09 We'll also create a CGPoint named xOnlyLocation, which will use the x 18:15 value from currentPoint, but always use self.view's center.y value. 18:20 We'll be using this to only drag the tray on the x-axis and 18:29 ignore changes to the y value during pan gestures. 18:32 Now, we need to create some if-else statements here to check the state of 18:36 the gesture recognizer. 18:40 We'll be doing different things if 18:41 the gesture state is began, 18:47 changed, ended, or cancelled. 18:53 Ended or canceled will be handled the same way, so 19:13 we bundle them together in our if-else branching. 19:16 We need to create another property in the interface for a UIAttachmentBehavior. 19:19 Back in the pan method under stateBegan, we'll create the attachment behavior. 19:36 We'll write self.panAttachmentBehavior equals UIAttachmentBehavior 19:54 alloc initWithItem self.trayView attachedToAnchor xOnlyLocation. 20:00 So, the item the behavior is attached to is our TrayView and 20:06 it's anchored to our xOnlyLocation CGPoint. 20:10 There are several properties on UIAttachmentBehavior that 20:14 modify how tightly the item sticks to this anchor, but 20:16 the out of the box values here will work for our needs. 20:20 Have a look at its documentation for more info. 20:23 Next, we add this behavior to our animator. 20:25 [SOUND] Now, under state changed, 20:28 we're just going to update the anchor point 20:32 property on self.panAttachmentBehavior. 20:37 Again, it's set to the xOnlyLocation. 20:42 Under ended slash cancelled, we remove the attachment behavior. 20:46 [SOUND] So we call self.animator, 20:49 remove behavior, 20:55 self.panattachmentbehavior. 20:58 Next, under ended/cancelled, 21:03 we're going to get the velocity of the gesture recognizer. 21:05 Velocity's returned as a CG point and indicates how fast the gesture is. 21:11 I've played around with these velocity values a bit. 21:16 And found that if the velocity.x value is over 500, 21:18 it feels like a pretty intentional throw of the tray view. 21:21 So we'll set this as our velocityThrowingThreshold. 21:26 Next we'll check if the absolute value of this velocity.x is over the threshold 21:29 And we'll initiate a throw of the view to either the left or right. 21:40 We check the absolute value because a throw to the left would be a negative x 21:44 and a throw to the right would be a positive x. 21:48 [SOUND] So we'll update the gravity 21:53 depending on whether or not x is positive or 21:58 negative allowing the view to fall 22:04 in the direction of our throw. 22:09 Next, we'll add an else statement after the throw check. 22:13 If the gesture has ended and 22:17 the user wasn't throwing the view, then we should either leave the tray open or 22:19 dismiss it to the right, depending on where it was let go. 22:23 So we'll set the gravity to left if the tray view's left edge is 22:26 left of the main view center. 22:30 Or gravity will be to the right if tray views left edge is right of center. 22:32 [SOUND] If we launch the app, 22:36 we can now test it, 22:42 swiping from the right edge. 22:46 Our tray view appears with its visual effect view background in basic text. 22:52 If we let go of it with its left edge beyond center screen 22:56 then it drops to the left. 23:00 If we drag it back to the right and 23:02 let go beyond center screen then it drops back to the right. 23:03 And if we throw the tray with enough velocity 23:07 it ends up going where we'd expected. 23:10 Great. 23:12 But if we try to rotate the app at this point, it causes some undesired results. 23:14 Let's go back to the app and add the new iOS8 rotation method view will 23:22 transition to size with transition coordinator. 23:27 [SOUND] Here we'll first 23:30 remove the behaviors 23:36 from the animator. 23:41 We'll add them back after the rotation next, 23:46 we want to check if the tray is open. 23:48 We'll again check if 23:51 the trayView's left edge is 23:55 left of center in the main view. 24:00 [SOUND] Here's where our tray left edge constraint comes into play. 24:05 We're going to update the constant on this constraint to place the tray in either its 24:13 visible or hidden position. 24:17 If we're left of center, we'll set its constant to the gutter width. 24:19 Next, we'll specify that gravity is left equals yes. 24:23 If we're right of center, in our else statement, 24:30 we're going to set the constant to size.width. 24:32 This size value is what the size will be when the rotation completes. 24:36 So the tray's left edge will be equal to the main view's left edge. 24:41 Plus the full width of the view. 24:45 In other words, it will be off screen to the right. 24:48 Next, we're going to call a method on the coordinator that is useful in this new 24:51 viewal transition to size method. 24:55 It's called animate alongside transition. 24:57 And, it allows you to perform your own additional animation during rotation if 25:00 necessary, and it gives you a completion block to do any cleanup or 25:04 setting up, which is exactly what we'll do. 25:08 We'll call setup behaviors on completion. 25:11 SOUND] In the animation block itself, 25:13 we'll just call self.view 25:22 layout if needed to 25:29 animate the constrain 25:33 update we've made. 25:38 If we run the app again in the simulator, we'll see that we can now rotate the view, 25:43 and open and close the tray as desired. 25:47 We can open the tray, rotate the view and the tray behaves properly. 25:51 Basically we're removing the physics behaviors, rotating and 25:56 allowing auto layout to draw the view properly, 26:00 then reapplying the physics behaviors to be used in the new layout. 26:02 Finally, let's add a close button to the tray to demonstrate one more useful 26:07 physics behavior. 26:11 Inside our setup tray view method, at the end right before layoutIfNeeded, 26:16 let's create a close button with type system. 26:20 We'll set translatesAutoResizingMaskIntoConstraints 26:24 to no. 26:27 Then set its title to close for state UI control state normal. 26:33 Then we'll set content horizontal alignment to UI control content 26:42 horizontal alignment left. 26:46 This basically just aligns our text left in the button. 26:56 Then we'll add 26:59 a method call, 27:03 closeButtonPressed. 27:08 With closeButton addTarget:self action:selector [closeButtonPressed] 27:14 forControlEvents:UIControlEventTouchUpIns- ide. 27:20 And finally, we'll add it to the tray view. 27:24 [NOISE] Next, we'll add some constraints. 27:26 We'll give it 30 points of left padding and a width of 75 points. 27:34 [SOUND] And for 27:37 the vertical 27:42 constraints we'll 27:46 give 30 points 27:53 of vertical padding and 27:59 a height of 40 points. 28:07 Now let's write the close button pressed method. 28:25 In it we'll create a UI push behavior. 28:35 We initialize it with an array of items which will just contain our tray view. 28:39 [SOUND] We set 28:42 mode to UI push 28:47 behavior mode 28:53 instantaneous. 28:59 Which means that this will be a one time push event. 29:06 There's also an option for continuous, which would continue to apply the push. 29:09 We'll set the angle radiance to zero, which pushes to the right. 29:14 Then we'll set it to magnitude to 200, which is a subjective value, and 29:18 relates to underlying physics relationships. 29:23 This is how hard the view is going to get pushed. 29:26 See the Apple documentation for 29:29 more explanation on how to calculate these force values. 29:30 Next we'll call updateGravityIsLeftNO, setting the gravity back to the right. 29:34 [SOUND] Then we'll add the push behavior to the animator 29:39 if we relaunch the app and pull the tray out from the right, 29:45 we'll see our new close button in the top left of the tray. 29:51 When we tap this button, it activates the one time push behavior and 29:58 our tray view is pushed off the screen to the right, and since we also set 30:02 the gravity to be to the right, the view falls into its offscreen position. 30:06 Great!. 30:12 So we've learned how to use four different physics behavior. 30:13 A gravity behavior, a collision behavior for our edges, 30:16 an attachment behavior for our pan gestures and a push behavior for our. 30:20 Close button. 30:24 There's plenty more to discover surrounding Apple's UI dynamic animator 30:25 and hopefully this demo has shown how exciting it can be to integrate 30:29 physics driven objects into your user interface work. 30:32 Thanks for watching. 30:36
You need to sign up for Treehouse in order to download course files.Sign up