This workshop will be retired on May 31, 2020.
Custom View Controller Transitions38:37 with Josh Timonen
This workshop focuses on creating a custom transition for a modal view controller’s presentation and dismissal. Instead of relying on Apple’s standard modal transitions, such as the slide-in from the bottom of the screen, cross-dissolve, flip, etc. we will create our own custom transition.
[MUSIC]. 0:00 [SOUND] Starting in iOS7. 0:03 Apple added the ability to create custom transitions between view controllers. 0:06 Today we're going to focus on creating a custom transition for 0:11 a modal view controller's presentation and dismissal. 0:14 So instead of relying on Apple's standard modal transitions, 0:18 such as the slide in from the bottom of the screen, cross dissolve, 0:21 flip, etc., we'll create our own custom transition. 0:25 [SOUND] For our workshop demo, we're going to build a simplified version of a photos 0:28 app, where users tap on an image thumbnail to view the image in a full screen modal. 0:32 We'll use a custom view controller transition to launch and 0:37 dismiss a view controller. 0:40 So let's get started. 0:42 Let's create a new single view project in x code. 0:52 We'll call it transitions demo. 0:56 We're going to add an image into the project, 1:07 which we'll be using in our views. 1:09 In the viewercontrol.m file infuted load we'll create a button that 1:23 displays an image. 1:27 [NOISE] So UIButton button equals UIButton buttonWithType: UIButtonTypeCustom, 1:44 We'll add target self with selector present modal. 1:50 [SOUND]. 1:54 Then, we'll set the button's image to the one we added to the project. 2:03 We'll set translatesAutoresizingMaskIntoConstraints 2:20 to no, then add it as a subview of self.view. 2:27 Then, we'll add some basic constraints, 2:32 making it 150 point square 2:41 with 30 points of top and 2:46 right padding. 2:51 [SOUND] The vertical constraints will just copy 2:54 the horizontal constraints [SOUND] and 3:01 will change h to v and update the string. 3:07 [SOUND] So we'll have a square button with our image on it, 3:14 150 point square in the top right corner. 3:17 Next, let's create that present modal method. 3:20 In it, we'll be presenting our modal, so we'll need to create that view now. 3:30 In X code, we'll go to File, New File, and select Coco touch class, and then name 3:36 our file custom modal view controller, a subclass of UI view controller. 3:42 Hit next, then create, and our class is added to our project. 3:48 Let's import CustomModalViewController.h at the top of ViewController.m. 3:52 [SOUND] And then down in present modal, 4:00 we'll create a new instance 4:07 of the view controller. 4:12 [SOUND] Then we'll call presentViewController, 4:16 animated, yes, with an empty completion block. 4:20 Next, let's open CustomModalViewController.m and 4:24 under viewDidLoad let's add our full screen image and a close button. 4:27 To add some visual separation, let's first set self.views background color to black.. 4:32 [SOUND] Next, let's build our close button. 4:37 We'll create it with type custom. 4:45 [NOISE] Then, we'll set 4:47 translate auto resizing 4:51 mass into constraints to know. 4:56 We're just going to keep this simple. 5:03 So the close button will just be the letter x. 5:04 We'll just set its title to the string, x. 5:07 Then we'll set its title label font to system font of size 5:13 twenty-four. 5:16 We'll set its title color to light-gray color. 5:23 Then add target self, with action close button pressed. 5:33 [SOUND] Finally, we'll add the button 5:37 as a subview of self dot view. 5:43 We want to position this button in the top right corner of our view. 5:49 So we'll create some auto layout that makes it 40 point square 5:53 with 10 points padding on the right, and 22 points padding above to account for 5:57 the status bar. 6:02 [SOUND]. 6:03 Next we'll create our full-screen image view. 6:40 We'll create our instance, UI image view image view equals UI image view new. 6:51 Set it's content mode to fit. 6:57 Set translates auto resizing mask into constraints to no. 6:59 Set the image to our mabelFlip image. 7:03 [SOUND] Then, add it as 7:06 a subview of self.view. 7:11 [SOUND] Then, we'll setup imageView's constraints pinning it to the edges of its 7:17 super view, which is self.view. 7:21 Next we'll create that close button pressed method. 7:40 [SOUND] Inside it, we'll call self 7:43 dismiss view controller animated, 7:49 yes, with completion nill. 7:55 Now, lets launch the app, we can see our 150 by 150 point button, 8:00 containing our image. 8:04 If we tap it, we see our modal with the full screen image enclose button 8:11 in the tope right corner. 8:15 Tapping that close button dismisses our modal. 8:16 Great. 8:19 Of course we don't have any custom transitions yet. 8:20 We didn't specify anything for the modal presentation. 8:22 So it's defaulting to the slide up from bottom animation you're probably 8:25 familiar with. 8:29 Back in viewController.m, in our presentModal method, we're going to set 8:30 the modal presentation style on the controller to UIModalPresentationCustom. 8:35 If we click on UI modal presentation custom and bring up the quick help in 8:44 the utility sidebar in X code, we can see a quick description of this value. 8:48 If we tap on UIViewController class reference, 8:53 the documentation gives us a little more information. 8:56 It explains that we also need to set the transitioning 9:00 delegate on the view controller, and we have this 9:03 UI view controller transitioning delegate protocol to conform to. 9:05 So let's also set that transitioning delegate to self on the modal. 9:09 [SOUND] And up in our private 9:13 interface we'll add 9:19 the protocol UI view controller 9:24 transition delegate. 9:31 [SOUND] If we command on this protocol we can see that it contains several optional 9:37 methods. 9:41 We're going to grab two of these, the animationControllerForPresentedController 9:42 method and the 9:46 animationControllerForDismissedController method. 9:47 We'll copy these into our view controller's implementation, and 10:06 in both we'll return self. 10:10 What we're doing here is telling the modal that this view controller is going to be 10:12 its transitioning delegate. 10:16 That's step one. 10:18 Then we're also saying that when it wants an animation controller for 10:19 presenting or dismissing the modal Then this view controller will also be that. 10:23 You'll notice that we're getting warnings on the return self-line 10:27 of each of these delicate methods. 10:30 They're expecting the object to subscribe to another protocol, 10:32 UI View controller animated transitioning. 10:35 Which our view controller class isn't subscribed to yet. 10:39 So let’s add that to our private interface above. 10:42 Now if we command click into that protocol's implementation 10:46 we can see the methods it defines. 10:49 We have two required methods and one that is optional. 10:51 We'll just copy the two required ones into our view controller's implementation. 10:55 For transition duration we'll just return 0.5. 11:13 Animate transition is going to be where we define the custom transition itself first, 11:17 we're going to want to keep a reference to the current transition context, 11:24 so lets add that as a property in the private interface. 11:31 [SOUND] In animate transition, we'll set self dot transition context to 11:37 the transition context that gets passed into this method. 11:43 Now we're going to make use of this transition context object. 11:58 If we command click into the UI view controller context transitioning protocol, 12:02 we'll see many methods we can call on this transition context object. 12:07 We'll just be using this view for key method. 12:11 [SOUND] You'll notice this is IOS eight only. 12:16 If you need to support IOS seven, you'll want to instead use the similar view 12:19 controller for key methods, then access their views. 12:23 Back in the animate transition method, we'll use this method with two keys. 12:27 UI transitionContext From View key. 12:31 And UI transitionContext to view key. 12:45 So from view equals transitionContext view for 12:55 key UI transition context from view key, and 12:57 to view will equal transition context view for key UITransitionContextToViewKey. 13:01 We now have a reference to the UI view that presented the modal, and 13:07 the view that is being presented. 13:10 Our animation code is going to animate our modal view from the buttons frame 13:12 to full screen. 13:17 To help with this, we'll create two frame properties in our private interface. 13:18 ModalCollapsedframe [NOISE] and 13:22 modalExpandedframe. 13:28 We'll also create a property for our button, 13:32 which is what we'll use to create the collapsed frame. 13:36 [SOUND] In animate transition, 13:41 we'll set the modalCollapsedFrame to the buttons frame. 13:44 Then we'll set the modalExpandedFrame to transition context containerview.bounds. 13:57 This container view for the transitionContext is the view that acts as 14:13 the super view for the views involved in this transition next we need to 14:17 discuss an interesting aspect of these custom transitions. 14:21 If you're presenting, the to view is the modal and 14:24 the from view is the view that presented it. 14:28 If you're dismissing, they're the opposite. 14:32 To view Is the presenter, and from view is the modal. 14:35 It makes sense if you think about it, but 14:39 it means that we need to be able to check which is which. 14:41 Since we're setting our view controller as the animation controller for 14:44 both presented controller and dismissed controller, 14:47 this animate transition method gets called in both cases. 14:51 So let's create a BOOL property in our private interface, called isPresenting. 14:55 Now in present modal, let's set is presenting to yes, 15:01 right before present view controller. 15:05 Then set it back to no, in the completion block. 15:08 Now, back in our animate transition method, 15:15 we'll declare a UI view called modal view and a CG rect destination frame. 15:18 We'll set these in a minute. 15:36 Next, we'll create an if statement. 15:38 If self isPresenting, modalView will equal toView, and 15:41 destination frame will equal modal expanded frame. 15:45 Else, modal view will equal fromView, and destinationFrame, 15:52 will equal the modalCollapsedFrame. 15:56 Now, if we're presenting, there's something we’re expected to do, and 16:01 that's add the modal to the transition context container view. 16:05 [NOISE] We don't actually have 16:08 to ever remove this. 16:16 The animator will do that for us, but we do have to remember this when presenting. 16:21 Otherwise, no modalView will appear. 16:25 Also, if we're presenting, we want to first start the modal view 16:28 at the frame of the button so that it animates from that framed to full screen. 16:32 So, we'll set it's frame to the collapsed frame, 16:37 and then call layout if needed on the view. 16:39 Now, for the actual animation. 16:43 Let's create a new method. 16:45 [SOUND] We'll call it animate with modal view passing in a UI 16:47 view destination frame passing in the destination frame. 16:52 In this method, we'll create our animate with duration call and 17:11 I'm going to use the spring with damping variation with some values I like. 17:15 And you'll notice I'm using the transition duration method, 17:53 passing in our transition context property. 17:56 Although in our case, the method really doesn't do anything with it. 17:58 Inside the animation block, we'll set the views frame to our destination frame and 18:02 call layout if needed on it. 18:07 [SOUND] In the completion block, we'll call 18:08 complete transition on the transition context. 18:13 We'll pass in yes. 18:18 This call tells the transition context that we're done transitioning, so 18:20 at the bottom of our animate transition method 18:25 let's call this new animation method. 18:27 We'll call self animateWithModalView, modalView destinationFrame, 18:29 destinationFrame. 18:34 One final thing I forgot to do was set our button property, 18:36 self dot button, to the button we created. 18:39 I'll do that now. 18:41 [SOUND] So now, if we launch the app, we can see that tapping 18:42 the button gives us the desired modal animation and 18:48 tapping the close button animates us back to the button's frame. 18:53 Great. 18:59 You'll leave a notice that we didn't do anything special in the code to dismiss 19:00 the model. 19:03 We just called the standard dismiss modal view controller method. 19:04 The modal's transitioning delegate is simply called and it behaves a specified. 19:09 And since our modal's layout was done with auto layout 19:14 if you animate smoothly through the transition, positioning elements properly. 19:16 The image view is pinned to the edges of its superview, which at the beginning of 19:21 the transition are in the same position as the view controllers button frame. 19:25 As it animates up to full screen, the image view and 19:29 the close button on the top right position as expected. 19:32 Now, wouldn't it be cool if we could pinch to dismiss this modal? 19:35 And wouldn't it be cool if the animation would track our finger through 19:40 the gesture? 19:42 To do this we're going to create an interactor object. 19:44 We'll go to file, new file Then select coco touch class and we'll name it 19:47 interactor which will be a subclass of UI percent driven interactive transition. 19:52 This is the class Apple has provided for us to subclass and manage a transition 19:58 that we can actually control at every percentage step of the way. 20:02 Click Next, then create. 20:06 [NOISE] If we go to the files header and command-click on the super class, 20:08 we'll see a few properties and then three methods at the bottom. 20:14 Let's copy these into our files implementation. 20:20 [NOISE] So we have updateInteractiveTransition, 20:22 cancelInteractiveTransition and 20:27 finishInteractiveTransition now, 20:31 we're going to move all of our transitioning delegate and 20:35 animated transitioning protocol methods in our view 20:41 controller's implementation into this interacter's implementation. 20:47 We'll also move our animate with modal view method. 20:55 After we cut and paste, you'll see we need to bring over several properties too. 21:10 So we'll move these into our interacters interface. 21:15 We'll put the frames and the BOOLs in the public interface, and 21:18 the transition context in the private interface. 21:21 [NOISE] We don't actually 21:24 have a private interface 21:30 generated for us yet. 21:37 So we'll create one at the top of the dot M file. 21:43 We also need to bring over the protocol definitions. 21:47 We'll put these in the public interface. 21:50 [NOISE] In our view controller dot M file lets create an interacter property. 21:54 We'll first need to include its header, 22:02 then create the property. 22:09 [NOISE] In view did load, 22:13 we'll create the interactor. 22:17 Now, under presentModal, 22:32 we'll update the transitioning delegate on the modal, to self.interactor. 22:33 We also need to update our isPresenting bool, 22:38 since it's now a property on the interactor. 22:41 We still have en error in our interactor.m. 22:45 In animate transition we don't have access to the button property anymore, so 22:48 we'll need to remove this line. 22:52 [NOISE] We'll need to get this frame elsewhere. 22:54 Back in ViewController.m, in presentModal, we'll set the modalCollapsedFrame 22:59 on the interactor to the button's frame right before presentation. 23:04 [NOISE] Above this method, 23:08 I'm going to override view did layout subviews. 23:12 We'll first call super in it. 23:20 What we want to do is update the modal collapsed frame. 23:24 To the button frame, whenever the view may change. 23:27 So we'll check to make sure the interactor and button both exist, and 23:31 if so, we'll update this frame. 23:35 For our testing, this will get hit after the view rotates and 23:38 the button frame may be different than before. 23:42 Now if we run the app, we'll see that it’s working just as before. 23:45 Our refactor hasn't broken anything. 23:50 Good. 23:51 Let's go back to our interactor.h file and 23:52 Cmd+click on the UI view controller transitioning delegate protocol. 23:55 [NOISE] We want to copy the two interaction controller methods and 23:59 paste them into our interactor's implementation. 24:06 We'll return self in both of these. 24:33 You'll notice that again we have a protocol that our class needs 24:35 to subscribe to. 24:39 UIViewControllerInteractiveTransitioning. 24:40 If we Cmd+click into that, 24:44 we'll see that it has one methods start interactive transition. 24:46 We'll copy and paste that into our implementation since we have update, 24:50 cancel and finish interactive transition methods that we're overriding in 24:54 this class, we'll put it right above those. 24:58 There's one more thing we need to do with these interaction controller methods. 25:04 If we just returned self all the time we're essentially telling the custom 25:08 transition that we always want it to be an interactive transition. 25:12 We have to check to see whether we are doing a gesture based interactive 25:16 transition or the regular button press transition 25:19 which means we just want our standard custom animated transition to get called. 25:23 So we'll add a property to our public interface, a bool called isInteractive. 25:26 [SOUND] We'll set this to yes when we're 25:32 starting a gesture based transition. 25:37 Back in the implementation, we'll add a ternary operator, and 25:42 if we're interactive, return self, otherwise, return nil. 25:46 [SOUND] Next, let's flesh out 25:50 our interactive transition. 25:55 Under start interactive transition, 26:00 we'll save the transition context passed in, setting it to our property. 26:02 And we'll set the modal expanded frame to the transition context container view's 26:07 bounds. 26:12 Inside update interactive transition, we're going to write the code that 26:15 collapses the modal, based on percent complete. 26:18 This is where you're free to do something creative. 26:22 What we'll be doing is generating a frame that scales down from full screen 26:25 to the presenting buttons frame, just like the regular animated transition. 26:29 Percent complete will range from 0 to 1. 26:33 In a few moments, 26:36 we'll write a pinch gesture that passes its scale into this method. 26:37 We know that the pinch will start at a scale of one, 26:41 then reduce down toward zero as the fingers come together. 26:44 Whether your internal math here goes from zero to one or one to zero is up to you. 26:49 To keep things simple, we'll be passing in the pinch's scale, 26:54 expecting a transition from zero to one. 26:58 To keep things simple, we be passing in a pinch of scale, 27:01 expecting a transition of zero to one. 27:04 The first thing we'll do is get a delta or difference value for the origin x, 27:07 origin y, width, and height values of our expanded and collapsed frames. 27:12 We'll do this by subtracting the collapsed destination value 27:18 from the expanded starting value. 27:21 So for the x origin delta. 27:24 Will be self dot modal expanded frame dot original dot x minus self dot modal 27:26 dot collapsed 27:31 frame dot x. 27:37 [SOUND] The why origin delta will be the same. 27:44 The modal expanded y minus the collapsed y. 27:48 [SOUND] The x width 27:51 delta will be 27:57 the expanded 28:02 width minus 28:07 the collapsed width. 28:11 And the y height delta will be the expanded height minus the collapsed 28:36 height. 28:40 Next, we'll create a transition value for each of these four parameters, 28:41 by starting with the collapsed frames value, then adding the calculated delta 28:45 multiplied by our current percent complete. 28:50 In other words, we'll be trying to figure out 28:53 where should our modals frame be at this percent in the transition? 28:56 Starting with the collapsed frame's values, 29:00 we add on a percentage of the difference between the expanded and collapsed frames. 29:02 [NOISE] So the transition x will 29:07 be the modal collapsed frame origin x plus. 29:12 The delta for the x times percent complete. 29:21 And the transition y will be the same with the y value. 29:25 And again, the transition width and height will be 29:27 the collapsed values adding their delta multiplied by percent complete. 29:32 [SOUND] In copying and pasting, 29:37 be sure that your values are correct. 29:42 Next, we need to get a reference to our modal view from the transition context. 29:48 Since we're only doing the gesture on dismissal, 29:53 we know it's going to be the from view So we'll call 29:56 self.transitionContext viewForKey UI transition context from view key. 29:59 Next, we'll set the from views frame to our transition origin x and 30:04 origin y, transition width, and transition height values. 30:09 Now, under finish interactive transition and cancel interactive transition, 30:22 we're basically going to call our animate with modal view method we created, 30:27 either dismissing down to the button frame, or cancelling the dismissal, and 30:31 animating back to full screen. 30:35 In both methods, we'll copy and paste in the from view line from the update 30:38 interactive transition, where we get it from the transition context. 30:42 We now need to make a small update to our animate with modal view method. 30:46 When we call it from cancel interactive transition, 30:50 we don't want to call complete transition yes in the animation completion block. 30:53 We want to say no instead so let's add didComplete as a bool variable 30:57 that gets passed into the method. 31:03 Then when we call complete transition, we'll just pass in didComplete. 31:11 This means we'll need to update the method call at the end of animateTransition, 31:16 passing in yes for didComplete in this case. 31:19 Then in finishInteractiveTransition we'll call animateWithModalView, 31:22 passing in the collapsed frame for the destination frame and yes for didComplete. 31:26 And in cancelInteractiveTransition we'll call the same method to pass in 31:37 the expanded frame for the destination frame, and no for did complete. 31:42 There's an optional method under UI view controller animated transitioning 31:52 named animation ended. 31:56 We'll add this method to our interacter and use it to clean up a bit. 31:57 We'll set our isInteractive and 32:01 isPresenting BOOL 32:09 values to no whether or not 32:14 the transitionCompleted 32:21 value is yes or no. 32:28 This way, we're in a good default position for whatever the user may do next. 32:34 Now, we'll go to our custom modal view controllers implementation and 32:38 set up the pinch gesture along with some other details. 32:42 For the pinch gesture to talk to our interactor, we'll need to have a way for 32:45 them to communicate. 32:48 Let's create an init method that passes in an interactor object. 32:50 We'll first need to import our interactor header file. 32:54 Then we'll add a property for the interactor in our private interface. 32:57 Then we'll create our new init method, 33:09 setting our interactor property to the one passed in. 33:11 So we'll write, instance type, 33:20 init with interactor, interactor, 33:24 if self equals super init, return self. 33:29 And inside the if statement, our interactor property equals interactor. 33:34 We'll also going to expose this init method in the header. 33:39 [SOUND] And we'll need to declare 33:42 the interactor class in the header as well. 33:47 Now, back in the dot m file, at the end of our view did load method, 33:59 let's set up a UIPinchGestureRecognizer, targeting self with the action 34:03 set to user did pinch, and then add the gesture recognizer to self.view. 34:08 So we'll write UIPinchGestureRecognizer PinchRecognizer 34:26 equals UIPinchGestureRecognizer alloc initWithTarget self action 34:31 selector userDidPinch Then self.view addGestureRecognizer pinchRecognizer. 34:37 Next we'll create this userDidPinch method. 34:44 [SOUND] Inside the method, we'll stem 34:47 out our three basic states began, 34:52 changed, and ended or canceled. 34:57 Inside began, we'll tell our interactor instance that is interactive is yes, 35:33 and that is presenting is no. 35:39 Since we're only doing an interactive dismissal, 35:41 we know that we are not presenting here. 35:44 Next we call dismissed view controller, 35:57 just as you would with the normal modal dismissal. 35:59 Inside changed, we'll first check that 36:01 the recognizer scaled is less than or equal to one. 36:07 In other words, we only want pinches that get smaller, 36:14 not pinches that might be a zooming in gesture. 36:17 Then, we'll call update interactive transition passing in the recognizers 36:20 scale value. 36:24 So we'll write, if recognizer scale is less than or equal to 1, 36:39 call self.interactor, update interactive transition, passing in recognizer.scale. 36:43 In ended or canceled, we'll check if the recognizer scale is beyond a certain 36:50 arbitrary point, which we’ll set to be 0.5, or half way. 36:54 If it's less than that value, 37:00 we'll call finish interactive transition on our interactor. 37:01 Otherwise, we'll call cancel interactive transition. 37:05 So we'll write another ternary operator here to call one of these two methods. 37:09 So we'll write recognizer.scale less than 0.5, if so 37:14 self.interactor Finish interactor transition. 37:18 Otherwise, self.interactor cancel interactor transition. 37:21 If you remember, these methods inside the interactor 37:25 animate the modal to it's expanded or collapsed frame. 37:28 Finally, we'll need to go back to our viewcontroller.m file and 37:32 update the custom modal view controllers instantiation with our new method 37:35 that passes in the interactor. 37:39 Now, if we build and run, we'll get to try out our new pinch gesture based dismissal. 37:45 If we launch the modal, then pinch to dismiss, and 37:50 when we've released the pinch beyond halfway, it closes. 37:53 If we pinch and release before halfway, it bounces back to full screen, great! 37:57 One final thing to show is that if we start in portrait, launch the modal, 38:06 then rotate to landscape, then start our dismissal, 38:10 it gets the proper collapsed frame for its final position. 38:14 This is because of our frame update code in view controllers viewdid 38:18 layout subviews. 38:21 So even though the button's position has changed, 38:23 we still animate out to the proper frame. 38:25 Great, that's it for this workshop. 38:29 I hope you've acquired some useful tools to help you build you own, 38:32 cool view transitions. 38:34 Thanks for watching. 38:36
You need to sign up for Treehouse in order to download course files.Sign up