- Custom Drawing
- Custom Transitions
- Wrapping Up
- Other Resources
Custom Transitions
iOS 7 provides classes and methods to support adding custom transitions to our scenes. Specifically, we can customize the transitions when presenting modal views, when switching between tabs, when pushing or popping views onto a navigation controller, and during layout-to-layout transitions between collection view controllers.
In this chapter, we will look at custom transitions when presenting view controllers and when switching between tabs.
For modal segues, the transitions give us a lot of control beyond just modifying the transition’s animation. We can also define the final shape and size of our presented view. Since the presented view no longer needs to cover the entire screen, our presenting scene remains in the view hierarchy. We may even be able to interact with any exposed portions of that original scene.
This is an important change, especially for the iPhone. In previous versions of the iOS SDK, presented views had to fill the entire screen on the iPhone (we had a few additional options on the iPad, but they were still relatively limited).
The ability to modify the tab bar animation is also a nice improvement. Before iOS 7, tab bar controllers could not use any animation at all. Touching a tab bar would instantaneously change the content.
However, we’re not just going to add an animation sequence between our tabs; we will also create an interactive transition, letting the users drive the transition using a pan gesture. As the user slides her finger across the screen, we will match the motion, sliding from one tab to the next.
Before we can do any of this, however, we need to understand how Core Animation works. Core Animation is the underlying technology behind all of our transitions.
Core Animation
I won’t lie to you: Core Animation is a rich, complex framework. Entire books have been written on this topic. There are lots of little knobs to tweak. However, UIKit has implemented a number of wrapper methods around the basic Core Animation functionality. This lets us easily animate our views, without dropping into the full complexity of the Core Animation framework.
The bad news is UIKit actually exposes two different animation APIs. The old API consists of calling a number of UIView methods. However, these methods must be called in the correct order or you will get unexpected behaviors. Furthermore, coordinating between different animations proved difficult.
While these methods have not been officially deprecated yet, their use is no longer recommended for any applications targeting iOS 4.0 or beyond. Since our minimum deployment target is iOS 4.3, there is no good reason to use these methods anymore.
Instead, we want to use UIView’s block-based animation API. UIKit provides a number of class methods in the block-based animation API. Some of these let us animate our views—moving them, resizing them, and fading them in and out. Others let us transition between views. They will remove a view from the view hierarchy, replacing it with a different view.
All UIViews have a number of animatable properties. These include frame, bounds, center, transform, alpha, backgroundColor, and contentStretch. To animate our view, we call one of the animation methods. Inside the animation block, we change one or more of these properties. Core Animation will then calculate the interpolated values for that property for each frame over the block’s duration—and it will smoothly animate the results.
If I want to move the view, I just change the frame or the center. If I want to scale or rotate the view, I change the transform. If I want it to fade in or fade out, I change the alpha. Everything else is just bells and whistles.
UIView’s animation methods are listed here:
- animateWithDuration:animations: This is the simplest animation method. We provide a duration and the animation block. It animates our changes over the given duration.
- animateWithDuration:animations:completion: This adds a completion block. This block runs after our animation has ended. We can use this to clean up resources, set final positions, trigger actions, or even chain one animation to the next.
- animateWithDuration:delay:options:animations:completion: This is the real workhorse. It provides the most options for simple animations. We can set the duration, a delay before the animation begins, a bitmask of animation options, our animation block, and a completion block.
- animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: This is a newer animation method. It’s used to create simple, spring-like effects. If you want to give the animation a slight bounce before it settles into the final state, this is your method. You can control the spring’s behavior by modifying the dampening and initial velocity arguments. We will look at other techniques for creating realistic, physics-based animations when we talk about UIDynamics in Chapter 8.
- animateKeyframesWithDuration:delay:options:animations:completion: We can use this method to run keyframe animations. These are more complex animations tied to specific points in time. To set up keyframe animations, you must call addKeyframeWithRelativeStartTime:relativeDuration:animations: inside this method’s animation block.
- addKeyframeWithRelativeStartTime:relativeDuration:animations: Call this method inside animateKeyframesWithDuration:delay:options:animations:completion:’s animation block to set up keyframe animations.
- performSystemAnimation:onViews:options:animations:completion: This method lets us perform a predefined system animation on an array of views. We can also specify other animations that will run in parallel. At this point, this lets us only delete views. The views are removed from the view hierarchy when the animation block is complete.
- transitionWithView:duration:options:animations:completion: We can use this method to add, remove, show, or hide subviews of the specified container view. The change between states will be animated using the transition style specified in the options. This gives us more flexibility than transitionFromView:toView:duration:options:completion:, but it requires more work to set up properly.
- transitionFromView:toView:duration:options:completion: This method replaces the “from view” with the “to view” using the transition animation specified in the options. By default the “from view” is removed from the view hierarchy, and the “to view” is added to the view hierarchy when the animation is complete.
- performWithoutAnimation: You can call methods that include animation blocks inside this method’s block. The changes in those animation blocks will take effect immediately with no animation.
Auto Layout and Core Animation
The easiest way to move views around using animation is to modify the view’s frame or center inside an animation block. This works fine as long as our view only has implicit auto layout constraints. In other words, we added a view to our view hierarchy, either programmatically or using Interface Builder, but we don’t provide any constraints for that view.
As long as a view has implicit constraints, we can continue to programmatically alter its position and size using the frame and center properties. However, once we start adding explicit constraints, things get more difficult. We can no longer think in terms of the frame or the center. If we try, we may get the view to move, but Auto Layout and Core Animation code will eventually fight for control over the view’s position. This can result in unexpected results. The animation may run to completion, only to have the view snap back to its original position. The animation may seem to work properly, only to revert after our device rotates or after some other call updates our constraints. Sometimes it can even cause views to flicker back to an old position briefly at the end of the animation. It all depends on the exact nature of the animation and the timing of the updates.
So, if we want to animate changes in size or position of a view with explicit constraints, we have two options.
- We can modify one of the existing constraints inside the animation block. This is the easiest option, but it is also more limited. We can change only the constraint’s constant property. Nothing else.
- We can remove the old constraint, add a new constraint, and then call layoutIfNeeded inside the animation block. This lets us make any arbitrary changes to our constraints but requires more work.
For the purpose of the Health Beat project, we will perform animation on views only with implicit constraints. These are easier to write and easier to understand. I recommend shying away from animating Auto Layout constraints until you have gotten very comfortable with both Core Animation and Auto Layout.
Customizing View Presentation
When creating our own transitions, we have two options: custom transitions and interactive transitions. Custom transitions work like most transitions we’ve seen. We trigger the transition, and then it automatically runs to completion. Most transitions occur rather rapidly, usually in 0.25 seconds.
On the other hand, the user drives interactive transitions. Before iOS 7, we had only one example of an interactive transition—the UIPageViewController. This is the controller behind iBook’s page flip animation. As you drag your finger across the screen, the page seems to curl, following your finger. You can move the finger forward or backward. You can stop, speed up, or slow down. The animation follows along. Lift your finger and the page either flips to its original position or continues to flip to the new page.
We will start with a custom transition since it is simpler. The actual transition isn’t too difficult—though it does have a number of moving parts to keep track of. However, it radically changes the behavior of our application, which will require some additional work to support.
Customizing Our Add Entry View’s Transition
Our application presents our Add Entry View controller as a modal view. It slides in from the bottom and then slides back out again when we are done. We want to change this. Instead of covering the entire screen, we’d like to cover just a small rectangle in the center. We’d also like to fade in and out, instead of sliding.
Doing this requires three steps.
- We set the presented controller’s modalPresentationStyle to UIModalPresentationCustom.
- We provide a transitioning delegate.
The transitioning delegate creates an object, which implements the actual animation sequences for us. It will actually provide two separate animation sequences: one to present our view, one to dismiss it.
Open HistoryTableViewController.m and add the following code to the bottom of its prepareForSegue:sender: method. This should go after the if block but before the closing curly bracket.
if
([segue.identifier
isEqualToString
:@”Add Entry Segue”
]) {UIViewController
*destinationController = segue.destinationViewController
; destinationController.modalPresentationStyle
=UIModalPresentationCustom
; destinationController.transitioningDelegate
=self
; }
Next, we need to have our history table view controller adopt the UIViewControllerTransitioningDelegate protocol. Scroll up to the top of the file, and modify the class extension’s declaration as shown here:
@interface
HistoryTableViewController
()<UIViewControllerTransitioningDelegate>
This should get rid of our errors, but if you run the app and bring up the add entry view, you’ll see that nothing has changed. Actually, the rotations no longer work properly—so things are worse than before.
We need to implement two of UIViewControllerTransitioningDelegate’s methods: animationControllerForPresentedController:presentingController:sourceController: and animationControllerForDismissedController:. However, before we do this, we need to create our animation controller class.
Animator Object
We need to create an object, which will manage our transition’s animation. In the Controller’s group, create a new Objective-C class named AddEntryAnimator. It should be a subclass of NSObject. Open AddEntryAnimator.h, and modify it as shown here:
@interface
AddEntryAnimator :NSObject
<UIViewControllerAnimatedTransitioning>
@property (assign, nonatomic, getter = isPresenting) BOOL presenting;
@end
We’re simply adopting the UIViewControllerAnimatedTransitioning protocol and declaring a presenting property.
Our AddEntryAnimator will be responsible for actually animating the appearance and disappearance of our Add Entry view. To do this, we must implement two methods: animateTransition: and transitionDuration:.
Switch to AddEntryAnimator.m. At the top of the file, let’s import AddEntryViewController.h. Next, let’s define a constant value before the @implementation block.
static const
NSTimeInterval
AnimationDuration =0.25
;
This is the duration we will use for our animations. We can now implement the transitionDuration: method as shown here:
- (NSTimeInterval
)transitionDuration: (id<
UIViewControllerContextTransitioning>
)transitionContext {return
AnimationDuration
; }
This method just returns our constant.
Now, let’s look at animateTransition: method. This method takes a single argument, our transition context. This is an opaque type, but it adopts the UIViewControllerContextTransitioning protocol. As we will see, the context provides a wide range of important information about our transition. This includes giving us access to our view controllers, the beginning and ending frames for their views (if defined by the transition), and our container view.
Our views and view controller hierarchies must be in a consistent state before our transition begins. They must also return to a consistent state after our transition ends. However, during the transition, they may pass through a temporary inconsistent state. To help manage this, we use a container view. The system will create the container and add it to our view hierarchy. It then acts as our superview during our animation sequence.
As part of animateTransition:, we need to perform the following steps:
- Make sure both views have been added to the container view.
- Animate the transition from one view to the next.
- Make sure each view is in its final position, if defined.
Call completeTransition: to end the transition and put things back into a consistent state.
Since we are using the same animator for both presenting and dismissing our views, our animateTransition: method must handle both cases. To simplify things, we will do some preprocessing in animateTransition:, but then call separate private methods to handle the actual animations.
Add the animateTransition: method, as shown here:
-(
void
)animateTransition: (id<
UIViewControllerContextTransitioning>
)transitionContext {id
fromViewController = [transitionContextviewControllerForKey
:UITransitionContextFromViewControllerKey
];id
toViewController = [transitionContextviewControllerForKey
:UITransitionContextToViewControllerKey
];UIView
*containerView = [transitionContextcontainerView
];if
(self
.presenting
) { [self
presentAddEntryViewController
:toViewControlleroverParentViewController
:fromViewControllerusingContainerView
:containerViewtransitionContext
:transitionContext]; }else
{ [self
dismissAddEntryViewController
:fromViewControllerfromParentViewController
:toViewControllerusingContainerView
:containerViewtransitionContext
:transitionContext]; } }
We start by requesting the “from” and “to” view controllers from our transition context. Then we ask for our container view. We check to see whether we’re presenting or dismissing our Add Entry view, and then we call the corresponding helper method.
When presenting view controllers, the from- and to-controllers can be a common source of confusion and errors. When our Add Entry View is appearing, the from-controller will be our RootTabBarController. This is the presenting view controller as defined by our current presentation context. The to-controller is our AddEntryViewController. However, when we’re dismissing the Add Entry View, these roles are reversed.
This means, sometimes our AddEntryViewController is the to-controller. Sometimes it’s the from-controller. By using helper functions, we can pass the toViewController and fromViewController in as more clearly named arguments. This lets us work with the parentController and addEntryController—instead of trying to remember which is “from” and which is “to” in each particular case.
Next, add the presentAddEntryViewController:overParentViewController:usingContainerView:transitionContext: method as shown here:
#pragma mark - private
- (void
) presentAddEntryViewController: (AddEntryViewController
*)addEntryController overParentViewController:(UIViewController
*)parentController usingContainerView:(UIView
*)containerView transitionContext: (id<
UIViewControllerContextTransitioning>
)transitionContext { [containerViewaddSubview
:parentController.view
]; [containerViewaddSubview
:addEntryController.view
];UIView
*addEntryView = addEntryController.view
;UIView
*parentView = parentController.view
;CGPoint
center = parentView.center
;UIInterfaceOrientation
orientation = parentController.interfaceOrientation
;if
(UIInterfaceOrientationIsPortrait
(orientation)) { addEntryView.frame
=CGRectMake
(0.0
,0.0
,280.0
,170.0
); }else
{ addEntryView.frame
=CGRectMake
(0.0
,0.0
,170.0
,280.0
); } addEntryView.center
= center; addEntryView.alpha
=0.0
; [UIView
animateWithDuration
:AnimationDuration
animations
:^{ addEntryView.alpha
=1.0
; }completion
:^(BOOL finished
) { [transitionContextcompleteTransition
:YES
]; }]; }
We start by adding both views to the container view. We want to make sure the parent view is added first, so our Add Entry view appears above it when drawn. According to the documentation, the system usually adds the from-controller to the container for us, but we typically have to add the to-controller ourselves. Personally, I find it easiest to just add both views. This won’t hurt anything, and it guarantees that both views are properly added and that they will be added in the correct order.
Next, we need to make sure our views end up in the proper position. Since we are presenting a view controller (instead of using the navigation controller or tab bar), our to-controller doesn’t have a preset destination. We get to define its final size and location. However, our parent view controller must not move. We could verify this by calling our transition context’s initialFrameForViewController: and finalFrameForViewController: methods for both view controllers. If it has a required position, it will return the frame for that location. If not, it returns CGRectZero instead—freeing us to do whatever we want.
We’re going to make our Add Entry view 280 points wide and 170 points tall. We are also going to center it in its superview. However, we need to make sure all of these coordinates are in the container view’s coordinate system, which is always in a portrait orientation.
Finally we perform the actual animation. We set our view’s alpha to 0.0, making it invisible. Then we use an animation block to fade it in, by setting the alpha to 1.0 inside the animation block. When the animation is complete, we call completeTransition:. This puts our view and view controller hierarchies into their final state and performs other cleanup tasks.
As you will see, the dismissal code is similar but much simpler. Add the following method, as shown here:
- (void
) dismissAddEntryViewController: (AddEntryViewController
*)addEntryController fromParentViewController:(UIViewController
*)parentController usingContainerView:(UIView
*)containerView transitionContext: (id<
UIViewControllerContextTransitioning>
)transitionContext { [containerViewaddSubview:parentController
.view
]; [containerViewaddSubview:addEntryController
.view
]; [UIView
animateWithDuration
:AnimationDuration
animations
:^{ addEntryController.view
.alpha
=0.0
; }completion
:^(BOOL finished
) { [transitionContextcompleteTransition
:YES
]; }]; }
This time, we don’t need to worry about calculating anyone’s frame. Again, the parent view should not move, while the Add Entry view will be removed from the view hierarchy and deallocated. So, we can safely ignore both of their frames.
We just add both views to the container view and then create our animation block. This time, we just set the Add Entry view’s alpha to 0.0 inside the animation block and then call completeTransition: in the completion block.
Implementing the Delegate Methods
Now, open HistoryTableViewController.m and import AddEntryAnimator.h. Then add the following methods after the storyboard unwinding methods:
#pragma mark - UIViewControllerTransitioningDelegate Methods
- (id<
UIViewControllerAnimatedTransitioning>
) animationControllerForPresentedController:(UIViewController
*)presented presentingController:(UIViewController
*)presenting sourceController:(UIViewController
*)source {AddEntryAnimator
*animator = [[AddEntryAnimator alloc
]init
]; animator.presenting
=YES
;return
animator; } - (id<
UIViewControllerAnimatedTransitioning>
) animationControllerForDismissedController:(UIViewController
*)dismissed {AddEntryAnimator
*animator = [[AddEntryAnimator alloc
]init
]; animator.presenting
=NO
;return
animator; }
The first method is called when performing the modal segue to our AddEntryView Controller. The second is called when our segue unwinds and our AddEntryViewController is dismissed. In both cases, we instantiate an animator. We set its presenting property, and we return the animator. The animator does all the real work.
Run the application, and display our Add Entry view. As you can see, there are a couple of problems. Our keyboard covers our view. Our weight label is truncated—we really want our text fields to shrink instead. Our view is not easy to see against the background. It does not rotate properly. When the Cancel or Save button is pressed, it is not dismissed. And, we can still use the controls of our background view. Click the Graph tab or the Edit button, and they operate normally.
As you can see, switching from presenting a modal view to presenting a custom pop-up can involve quite a few changes to our code.
General Cleanup
Let’s fix the easy things first. For whatever reason, when we use the regular modal presentation style, the segue unwinding methods will automatically dismiss our presented view controller. We didn’t have to do anything. However, with the custom presentation style, we need to programmatically dismiss our presented controller.
Still in HistoryTableViewController.m, navigate to our segue unwinding methods. Add the following line to the bottom of both addNewEntrySaved: and addNewEntryCanceled::
[self
dismissViewControllerAnimated
:YES
completion
:nil
];
Our Add Entry view will now be properly dismissed whenever the Save or Cancel button is tapped.
Next, open Main.storyboard and zoom in on the Add Entry View Controller scene. Our Auto Layout constraints did not work as we expected. Our labels are supposed to remain the same size, while our text fields grow and shrink. This worked properly when we expanded the view, but didn’t work now that we shrunk the view.
There were no unusual warnings in the console, so either we have a bug in our constraint logic or we have managed to create an ambiguous constraint. According to Interface Builder, everything’s OK, so let’s take a look at the compression resistance and hugging priorities for our labels and text fields.
Our labels have a content hugging of 251 and a compression resistance of 750. Our text fields have a content hugging of 100 and a compression resistance of 750. We reduced their content hugging to make sure they stretched when the view grew wider during rotations; however, we never thought about our views becoming smaller. Both the text field and the label have a content hugging of 750, which means we have an ambiguous layout. The system doesn’t know which element to shrink.
Drop the Horizontal Compression Resistance on both text fields to 100. This breaks the tie. Now, when we run the app, our text fields shrink instead of our labels.
This, however, introduces another problem. Our text fields are now too small for our date. Open AddEntryViewController.m and modify updateDateText as shown here:
- (void
)updateDateText {if
(self
.date
==nil
) {self
.dateTextField
.text
=@””
; }else
{self
.dateTextField
.text
= [NSDateFormatter
localizedStringFromDate
:self
.date
dateStyle
:NSDateFormatterShortStyle
timeStyle
:NSDateFormatterShortStyle
];self
.dateTextField
.textColor
= [UIColor
darkTextColor
]; } }
Now, back in Main.storyboard, select the date text field, and change its Min Font Size attribute to 12. That should both give us more space, and let the font shrink if necessary.
While we’re in Interface Builder, let’s do something about our view’s appearance. Select the View, and then click the Background attribute. In the pop-up menu, select Other.... This brings up the color picker. I want a very light blue—almost, but not quite, white. Select the Color Sliders tab, and set it to RGB Sliders. Set the Red to 240, Green to 245, and Blue to 255. Then close the color picker. This will help our Add Entry view stand out against the background.
Finally, let’s fix the rotation. Switch back to AddEntryViewController.m and add the following method just under HBT_updateFonts:
- (void
)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation
)toInterfaceOrientation duration:(NSTimeInterval
)duration { [self
centerViewInParent
]; }
This method is called from within our rotation’s animation block, so any animatable properties that we change here will be animated along with the rotation. We will be updating our view’s position in a few different methods, so we should extract that code into its own, private method. Add the centerViewInParent method to the bottom of our private methods, just before the @end directive.
- (void
)centerViewInParent {UIView
*parentView =self
.view
.superview
;CGPoint
center = parentView.center
; center = [parentViewconvertPoint
:centerfromView
:parentView.superview
];self
.view
.center
= center; }
Here, we calculate our center’s new position. Again, we grab the superview’s center and then convert it to the correct coordinate system. Finally, we assign it to our view.
This will cause our view’s location to update properly when rotated.
Avoiding the Keyboard
We need to listen to the keyboard notifications and move out of the way of our incoming keyboard. We also need to adjust our position if the keyboard is being displayed when a new text field is selected or when we rotate. This means we need to know the keyboard’s state.
Let’s start by listening to some of the keyboard appearance and disappearance notifications. Open AddEntryViewController.m. In the class extension, we need to add two properties for our observers. We also need a property to hold our keyboard’s frame and a property to record whether it’s currently being displayed.
@property
(strong
,nonatomic
)id
keyboardWillAppearObserver;@property
(strong
,nonatomic
)id
keyboardWillDisappearObserver;@property
(assign
,nonatomic
)CGRect
keyboardFrame;@property
(assign
,nonatomic
)BOOL
keyboardIsShown;
Now, after initWithNibName:bundle:, add a dealloc method to remove these observers.
- (void
)dealloc {NSNotificationCenter
*center = [NSNotificationCenter
defaultCenter
];if
(self
.keyboardWillAppearObserver
) { [centerremoveObserver
:self
.keyboardWillAppearObserver
]; }if
(self
.keyboardWillDisappearObserver
) { [centerremoveObserver
:self
.keyboardWillDisappearObserver
]; } }
Most of the time, we’ve removed our observers just because it’s a good habit to get into. However, since the view controllers in question would last throughout our application’s entire life cycle, it was not vital. In this case, however, it is required. Our Add Entry View Controller will be deallocated after it is dismissed. We must make sure it removes all its observers before it disappears.
Now, navigate to viewDidLoad: We’re going to add the following line to the bottom of the method (just after the call to setupInputAccessories):
[self
setupNotifications
];
Next, we need to implement this method. Add setupNotifications just after our centerViewInParent method:
- (void
)setupNotifications {NSNotificationCenter
*center = [NSNotificationCenter
defaultCenter];NSOperationQueue
*mainQueue = [NSOperationQueue
mainQueue];UIWindow
*window = [[[UIApplication
sharedApplication] delegate] window];__weak
AddEntryViewController
*_self =self
;self
.keyboardWillAppearObserver
= [center addObserverForName:UIKeyboardWillShowNotification
object:nil
queue:mainQueue usingBlock:^(NSNotification
*note) { _self.keyboardIsShown
=YES
;CGRect
frame = [note.userInfo[UIKeyboardFrameEndUserInfoKey
] CGRectValue]; frame = [window convertRect:frame fromWindow:nil
]; frame = [_self.view
.superview
convertRect:frame fromView:window]; _self.keyboardFrame
= frame;NSTimeInterval
duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey
] doubleValue]; [UIView
animateWithDuration:duration animations:^{ [_selfplaceFirstResponderAboveKeyboard
]; }]; }];self
.keyboardWillDisappearObserver
= [center addObserverForName:UIKeyboardWillHideNotification
object:nil
queue:mainQueue usingBlock:^(NSNotification
*note) { _self.keyboardIsShown
=NO
; _self.keyboardFrame
=CGRectZero
;NSTimeInterval
duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey
] doubleValue]; [UIView
animateWithDuration:duration animations:^{ [_selfcenterViewInParent
]; }]; }]; }
When the keyboard appears, we set keyboardIsShown to YES, and then we grab the keyboard’s end frame from the notification’s userInfo dictionary. We convert it into our superview’s coordinate system. Then we call a private method to update our position relative to the keyboard. We do the update in an animation block and set the animation duration to our keyboard’s appearance duration—this helps match our keyboard’s animation.
When the keyboard will disappear notification is received, we set keyboardIsShown to NO. We set the keyboard’s frame to all zeros, and we center our view in the frame. Again, we use an animation block to sync with our keyboard’s disappearance.
We still need to implement placeFirstResponderAboveKeyboard. Add this after our setupNotifications method.
- (void
)placeFirstResponderAboveKeyboard {UIView
*parentView =self
.view
.superview
;CGPoint
currentCenter =self
.view
.center
;UIView
*firstResponder = [self
.dateTextField isFirstResponder
] ?self
.dateTextField
:self
.weightTextField
;CGRect
textFrame = [parentViewconvertRect
:firstResponder.frame
fromView
:self
.view
];// our superview is always in portrait orientation
UIInterfaceOrientation
orientation =self
.interfaceOrientation
;if
(UIInterfaceOrientationIsPortrait
(orientation)) {CGFloat
textBottom =CGRectGetMaxY
(textFrame);CGFloat
targetLocation =
CGRectGetMinY
(self
.keyboardFrame
) - (CGFloat
)8.0
; currentCenter.y
+= targetLocation - textBottom; }else if
(orientation ==UIInterfaceOrientationLandscapeLeft
) {CGFloat
textBottom =CGRectGetMaxX
(textFrame);CGFloat
targetLocation =
CGRectGetMinX
(self
.keyboardFrame
) - (CGFloat
)8.0
; currentCenter.x
+= targetLocation - textBottom; }else
{CGFloat
textBottom =CGRectGetMinX
(textFrame);CGFloat
targetLocation =
CGRectGetMaxX
(self
.keyboardFrame
) + (CGFloat
)8.0
; currentCenter.x
+= targetLocation - textBottom; }self
.view
.center
= currentCenter; }
We start by grabbing our parent view and our current center. We want to make sure that all coordinates are in our parent view’s coordinate system, so we will convert our coordinates when necessary.
Next, we determine who our first responder is. Here, we use C’s ternary operator. Basically, it has three parts. It checks to see whether the first part (before the question mark) is true. If it is true, it returns the second part (between the question mark and the colon). If it’s not true, it returns the third part (after the colon).
We can then convert our first responder’s frame to the parent view’s coordinate system and calculate the distance we need to move our view. This gets a bit complicated, as you can see.
Here’s the problem. You might think that our Add Entry view’s superview would be the presenting controller’s view (e.g., the RootTabBarController’s view). This is not the case. It’s actually kept in the container view from our transition. And, just like inside our transition, the container view is always kept in portrait orientation. This means we have to adjust our coordinates appropriately.
Conceptually, we get the bottom of the selected text view and the top of our keyboard. We calculate the difference between them, adding in an 8-point margin. Then we offset our view’s center by that difference. Of course, the details vary depending on our current orientation.
This will shift our view so that the currently selected text field is just above the keyboard. Notice that it also works when we rotate, since our keyboard is removed and re-added during rotations.
Now, our keyboard moves out of the way—most of the time. However, there’s a problem when our view first appears. The keyboard appears as well, but the timing doesn’t quite work. The simplest solution is to move our call to becomeFirstResponder from viewWillAppear: to viewDidAppear:. This delays the appearance of our keyboard, making sure our view can move out of the way as expected.
Delete the following line from viewWillAppear::
[self
.weightTextField becomeFirstResponder
];
And, just after that method, implement viewDidAppear: as shown here:
- (void
)viewDidAppear:(BOOL
)animated { [super
viewDidAppear:animated
]; [self
.weightTextField becomeFirstResponder
]; }
One last tweak. If we are currently editing one text field and we select the other text field, our view should shift appropriately. Let’s start by adding an action method that the views can trigger when they begin editing.
Add this method right after the unitButtonTapped method:
- (IBAction
)textFieldSelected:(id
)sender {if
(self
.keyboardIsShown
) { [UIView
animateWithDuration
:0.25
animations
:^{ [self
placeFirstResponderAboveKeyboard
]; }]; } }
Here, we just check to make sure the keyboard is already showing. If it is, we adjust our view’s positioning inside an animation block with a quarter-second duration.
Now, let’s switch to Main.storyboard. Control-click the weight text field to bring up its connections HUD. Drag from Editing Did Begin to our Add Entry View Controller, and select textFieldSelected:. Do the same thing for our date text field.
That’s it. Our pop-up view now automatically adjusts its positioning relative to the keyboard. Run the app and test it. It would be nice to have a Next button in our toolbar to toggle between the text fields, but I’ll leave that as an exercise for the reader.
Making the View Modal Again
Currently, the users can interact with the scenes behind our Add Entry view. This doesn’t cause any real problems. You can actually use this to make sure our graph view is updating properly. With the History view showing, bring up the Add Entry view. Then tap the Done button to dismiss the keyboard. With our Add Entry view still showing, select the Graph tab. Now enter a weight. It should appear on the graph.
While this is somewhat interesting, I’d rather make our Add Entry scene behave like a modal view again. I think that would be less surprising and confusing to the users. Fortunately, this is rather easy to do. However, the solution is not obvious or intuitive.
We just need to modify the way our Add Entry view performs hit testing.
When a finger touches the screen, the system uses hit testing to determine exactly which element was touched. For each touch event, it asks the window if the touch was within its bounds. If it was, the window asks all of its children. If the touch was within any of their bounds, they ask all their children. This repeats all the way down the view hierarchy.
The system does this using the hitTest:withEvent: method. By default, if the touch event is outside the receiver’s bounds, it returns nil. Otherwise, it calls hitTest:withEvent: on all of its subviews. If any of them return a non-nil value, it returns the results from the subview at the highest index (the one that would lie on top of all the others). Otherwise, it returns itself.
So, we’ve already seen that our Add Entry view’s superview is our transition’s container view. This view fills the entire screen. So, no matter where the user touches on the screen, it will be inside the container’s bounds. The container will then call our view’s hitTest:withEvent: method. We just need to guarantee that our view never returns nil. This will allow our view to grab all the touch events whenever it is displayed.
So, let’s start by adding a new UIView subclass to the Views group. Name this class AddEntryView. Then open AddEntryView.m. We don’t need the initWithFrame: or the commented-out drawRect: methods. Delete them both. Then add the following method:
- (UIView
*)hitTest:(CGPoint
)point withEvent:(UIEvent
*)event {UIView
*view = [super
hitTest
:pointwithEvent
:event];if
(view ==nil
)return self
;return
view; }
Here, we call super’s hitTest:withEvent:. If this returns a value, we simply pass that value along. Otherwise, we return self. This lets our view’s subviews continue to respond to touch events but grabs and ignores all other touch events.
Now, open Main.storyboard. Zoom in on the Add Entry View Controller scene. Select the scene’s view, and in the Identity inspector, change its class to AddEntryView.
Run the application. When you bring up the Add Entry view, you can no longer touch any of the controls below our view. However, the keyboard still works, since it is displayed above our view.
Last Thoughts on Presenting Custom Views
The actual custom animation had a number of cogs and gears that needed to be connected correctly, but it really wasn’t that hard. We spent most of our time writing custom code to deal with the fact that our Add Entry view was no longer a full-screen, modal view.
In fact, we’ve written all of the custom code with the basic assumption that our Add Entry View Controller will always be presented using the UIModalPresentationCustom presentation style.
If we ever plan on pushing this controller onto a navigation stack, adding it to our tab bar, or even presenting it full screen using the UIModalPresentationFullScreen style, we would need to check and see how our view is being displayed and then enable or disable the code as appropriate.
We could use our view controller’s presentingViewController, navigationController, tabBarController, modalPresentationStyle, and similar properties to determine this. However, going through all the different corner cases gets quite involved.
The good news is if you mess this up, it will be immediately, visibly obvious. This is not one of those bugs that can sneak up and bite you unexpectedly.
Interactive Transitions
Now, let’s look at interactive transitions.
There’s good news, bad news, and good news again. Interactive transitions build upon everything we learned about custom transitions. They are a bit more complex, since we have to sync them with a gesture (or really anything that can programmatically drive our percentages). We also need to determine whether our transition is finished or whether it has been canceled.
Fortunately, we are just adding animation and a bit of additional interactivity to the tab bar controller—we’re not making fundamental changes to the way these scenes are presented, like we did with our Add Entry scene. So, once the animation is working, we’re done.
When presenting a view controller, we had to set the controller’s transitioningDelegate. Fortunately, when customizing the transitions for tab bars or navigation bars, the animation methods have already been added to the UITabBarControllerDelegate and UINavigationControllerDelegate protocols. So, we just need to create an object that adopts UITabBarControllerDelegate. Assign it as our tab bar’s delegate and implement the animation methods
Just like before, our delegate will return an animator object that adopts the UIViewControllerAnimatedTransitioning protocol. This will manage the noninteractive portions of our animation; for example, if you select a new tab, the animator object will automatically animate the transition from one scene to the next.
If we want to add interactivity, we also need to return an object that adopts the UIViewControllerInteractiveTransitioning protocol. This is responsible for the interactive portion. Interactively driving the animation could get quite complex. Fortunately, UIKit provides a concrete implementation, UIPercentDrivenInteractiveTransition, that handles most of the details for us.
The percent-driven interactive transition will actually use our animator object. It can access the frames that the animator object produces and display the correct frame based on the interaction’s current state.
While driving the animation interactively, we just change the percentage, and the animation updates appropriately. If we cancel the interaction, it will use the animator to roll back the animation to its beginning state. If we finish the interaction, it will use the animator to complete any remaining animation, bringing the views to their correct, final position.
Strictly speaking, we don’t need to subclass UIPercentDrivenInteractiveTransition, but I find that it usually simplifies our application’s logic. It also lets us combine our UIPercentDrivenInteractiveTransition and our UITabBarControllerDelegate into a single object.
Creating the Animator Object
So, let’s start with our animator object. In the Controllers group, create a new NSObject subclass named TabAnimator.
Now that we have a couple of animators, let’s give them their own group. Select TabAnimator.h, TabAnimator.m, AddEntryAnimator.h, and AddEntryAnimator.m. Control-click them and select “New Group from Selection” from the pop-up menu. Name the group Animators.
In my mind, these objects belong with the controllers, so I will leave this group inside the Controllers group. However, you could drag it up to the top level or into one of the other groups, if you wanted.
Now, select TabAnimator.h. Modify it as shown here:
@interface
TabAnimator :NSObject
<UIViewControllerAnimatedTransitioning>
@property (weak, nonatomic) UITabBarController *tabBarController;
@end
We are simply adopting the UIViewControllerAnimatedTransitioning protocol and adding a property that will refer back to our tab bar controller.
Switch to TabAnimator.m. At the top of the file, before the beginning of our @implementation block, we need to declare a constant for our duration. Add the following line of code:
static const
NSTimeInterval
AnimationDuration =0.25
;
Now, we can define our methods. As before, we have to implement only two methods. The first is transitionDuration:.
-(NSTimeInterval
)transitionDuration: (id<
UIViewControllerContextTransitioning>
)transitionContext {return
AnimationDuration
; }
Just like our AddEntryAnimator, this method returns our constant. After this, add the animateTransition: method.
-(void
)animateTransition: (id<
UIViewControllerContextTransitioning>
)transitionContext {UIViewController
*fromViewController = [transitionContextviewControllerForKey
:UITransitionContextFromViewControllerKey
];UIViewController
*toViewController = [transitionContextviewControllerForKey
:UITransitionContextToViewControllerKey
];NSUInteger
fromIndex = [self
.tabBarController
.viewControllers
indexOfObject
:fromViewController];NSUInteger
toIndex = [self
.tabBarController
.viewControllers
indexOfObject
:toViewController];BOOL
goRight = (fromIndex < toIndex);UIView
*container = [transitionContextcontainerView
];CGRect
initialFromFrame = [transitionContextinitialFrameForViewController
:fromViewController];CGRect
finalToFrame = [transitionContextfinalFrameForViewController
:toViewController];CGRect
offscreenLeft =
CGRectOffset
(initialFromFrame, -CGRectGetWidth
(container.bounds
),0.0
);CGRect
offscreenRight =CGRectOffset
(initialFromFrame,CGRectGetWidth
(container.bounds
),0.0
);CGRect
initialToFrame;CGRect
finalFromFrame;if
(goRight) { initialToFrame = offscreenRight; finalFromFrame = offscreenLeft; }else
{ initialToFrame = offscreenLeft; finalFromFrame = offscreenRight; } fromViewController.view
.frame
= initialFromFrame; toViewController.view
.frame
= initialToFrame; [containeraddSubview
:fromViewController.view
]; [containeraddSubview
:toViewController.view
];UIViewAnimationOptions
options =0
;if
([transitionContextisInteractive
]) { options =UIViewAnimationOptionCurveLinear
; } [UIView
animateWithDuration
:AnimationDuration
delay
:0.0
options
:optionsanimations
:^{ toViewController.view
.frame
= finalToFrame; fromViewController.view
.frame
= finalFromFrame; }completion
:^(BOOL
finished) {BOOL
didComplete = ![transitionContexttransitionWasCancelled
];if
(!didComplete) { toViewController.view
.frame
= initialToFrame; fromViewController.view
.frame
= initialFromFrame; } [transitionContextcompleteTransition
:didComplete]; }]; }
Phew, that’s a lot of code! Let’s walk through it. Just as before, we start by grabbing our “from” and “to” view controllers. Since we are adding a custom transition to a tab bar, we don’t have the same confusion we did with the custom presentation. We are always moving explicitly from one controller to another controller. The from-controller is the controller we started with. The to-controller is the new tab’s controller. We may not make it. Our transition may get canceled. But it is our intended destination.
Even if the interactive portion gets canceled, it doesn’t change these objects—our interactive transition will simply run this animation in reverse.
Next, we calculate the tab index of our from- and to-controllers, and we determine whether we are sliding to the right or to the left.
We grab our container and request the initial frame for our from- and the final frame for our to-controller. In theory, these should be the same frame, since our final view is replacing our initial view. However, I like to grab them as separate local variables—just in case something changes in future implementations.
Also, notice that, unlike the presentation transition, the transitionContext defines a final frame for our to-controller. This means our to-controller’s view must be in the specified position when the animation ends. However, the context does not specify a starting position for our to-controller. It also does not specify an ending position for our from-controller. Since we’re sliding views in and out, we’d like the to-controller to start just offscreen and the from-controller to end just off the opposite side. Whether they are sliding from the left or right edge depends on the transition’s direction.
Returning to our code, we take the from-controller’s initial frame and use CGRectOffset() to create new frames that are shifted horizontally just past the edge of the screen. These are our offscreenLeft and offscreenRight rectangles.
Notice that, unlike the presenting transition, the container view will rotate to match our tab bar’s orientation. This means we don’t need to modify our math based on our orientation.
Next, we use the offscreen rectangles to set our to-controller’s initial frame and our from-controller’s final frame. We make sure our views are in the correct starting position, and we add them to our container view.
There’s one last pretransition check. We see whether our animation is interactive. If it is, we’re going to set the animation curve to linear. If not, we will use the default curve.
By default, our animations are not displayed at a linear speed. Rather, they ease in and ease out. This means they start at a complete stop. They accelerate up to speed, and then they decelerate back to a stop. This gives the animation a nice appearance that mimics the way physical objects move. However, the ease-in-out animation curve is not always appropriate, so we can change the animation curve by passing the correct option to the relevant animation methods.
In this case, when we are presenting an interactive animation, we want to make sure our view stays under the user’s finger. That means we must use a linear animation. Otherwise, the view and finger will move at different rates.
Finally, we perform our animation. We have to use the longer animateWithDuration:delay: options:animations:completion: method, since we may be changing our animation’s curve.
In the animation block, we simply update the frames for both our “to” and “from” view controllers. In the completion block, we check to see whether the animation was canceled. If it was, we reset the frames to their initial positions. Finally, we call completeTransition:. We pass YES if the transition completed and NO if it was canceled. This should close out the transition and move the view and view controller hierarchies into their final states.
Building TabInteractiveTransition
Now let’s create our UIPercentDrivenInteractiveTransition subclass. This object will also act as our UITabBarControllerDelegate. Create a new Objective-C class in the Animators group. Name it TabInteractiveTransition and make it a subclass of UIPercentDrivenInteractiveTransition.
Next, open TabInteractiveTransition.h. We want to adopt the UITabBarControllerDelegate property and declare a public method, as shown here:
@interface
TabInteractiveTransition :UIPercentDrivenInteractiveTransition
<UITabBarControllerDelegate>
- (id)initWithTabBarController:(UITabBarController *)parent;
@end
Switch to TabIntractiveTransition.m, and import TabAnimator.h. And create a class extension with the following properties:
@interface
TabInteractiveTransition
()@property
(assign
,nonatomic
,getter
= isInteractive)BOOL
interactive;@property
(weak
,nonatomic
)UITabBarController
*parent;@property
(assign
,nonatomic
)NSUInteger
oldIndex;@property
(assign
,nonatomic
)NSUInteger
newIndex;@end
The interactive property will track whether we’re currently in an interactive transition. The parent property will hold a reference to our tab bar controller. Finally, the newIndex and oldIndex properties will hold the starting and ending tab indexes for the duration of our transition.
Now, add our init... methods. These go just after the @implementation line.
- (id
)initWithTabBarController:(UITabBarController
*)parent {self
= [super
init
];if
(self
) {_parent
= parent;_interactive
=NO
; }return self
; } - (id
)init { [self
doesNotRecognizeSelector
:_cmd
];return nil
; }
initWithTabBarController is our designated initializer. It just sets the parent property and starts with interactive set to NO. Then, as always, we override the designated init method of its superclass. This time, we throw an exception if this method is called. This forces us to always use our designated initializer.
Now, we have two animation methods from UITabBarControllerDelegate that we need to implement. The first returns our animation object.
#pragma mark = UITabBarControllerDelegate Methods
-(id<
UIViewControllerAnimatedTransitioning>
)tabBarController: (UITabBarController
*)tabBarController animationControllerForTransitionFromViewController: (UIViewController
*)fromVC toViewController:(UIViewController
*)toVC {TabAnimator
*animator = [[TabAnimator alloc
]init
]; animator.tabBarController
=self
.parent
;return
animator; }
Here, we just instantiate TabAnimator, set its tabBarController property and return it.
Testing the Custom Animation
Before we wire in our interactivity, let’s test the custom animation. Switch to RootTabBarController.m, and import TabInteractiveTransition.h. Then add the following property to the class extension:
@property
(strong
,nonatomic
) TabInteractiveTransition *interactiveTransition;
This will hold our interactive transition object.
Finally, at the bottom of viewDidLoad, add the following two lines of code:
self
.interactiveTransition
= [[TabInteractiveTransition alloc
]initWithTabBarController
:self
];self
.delegate
=self
.interactiveTransition
;
Here, we instantiate our TabInteractiveTransition object and assign it to our interactiveTransition property. We also assign it to our delegate property. This may seem like unnecessary duplication, but both steps are important.
We need to assign our object to our delegate property, since that’s where the real work is done. Unfortunately, delegate properties are almost always weak. This helps prevent retain cycles. Unfortunately, this also means that the delegate property, alone, won’t keep our TabInteractiveTransition object in memory. If we want it to last beyond this method, we need to assign it to a strong property as well. The interactiveTransition property simply exists to keep our TabInteractiveTransition object alive.
Run the application and tap the tab bars. Our custom animation will now slide between the History and Graph views.
Adding Interactivity
We have a custom animation, but it’s not yet interactive. Let’s fix that.
Open TabInteractiveTransition.h and declare two additional methods.
- (void
)handleLeftPan:(UIPanGestureRecognizer
*)recognizer; - (void
)handleRightPan:(UIPanGestureRecognizer
*)recognizer;
We will connect these to our gesture recognizer in a minute.
Switch to TabInteractiveTransition.m. We still have one more delegate method to implement.
- (id<
UIViewControllerInteractiveTransitioning>
)tabBarController: (UITabBarController
*)tabBarController interactionControllerForAnimationController: (id<
UIViewControllerAnimatedTransitioning>
)animationController {if
(self
.interactive
) {return self
; }return nil
; }
If we want to produce an interactive transition, we must implement both tabBarController:animationControllerForTransitionFromViewController: toViewController: and tabBarController:interactionControllerForAnimationController:. The interaction controller method will not be called unless the animation controller returns a valid object.
In this case, we simply return self if we’re in an interactive state. Otherwise, we return nil. Or, to put it more simply, if the user starts the animation with a gesture, this method will return self, and the animation will proceed interactively. If the user simply taps one of the tabs, it will return nil, and the animation will complete automatically.
Now, all we need to do is set up a gesture recognizer to drive the interactive transition.
Gesture Recognizers
There are two ways to handle touch events. First, we could use low-level touch handling to monitor each and every touch. Alternatively, we could use higher-level gesture recognizers to recognize and respond to common gestures.
Let’s look at the low-level approach first.
Whenever the user’s finger touches the screen, this creates a touch event. The system uses hit testing to determine which view was tapped. That view is then sent the touch event. If it doesn’t respond, the touch event is passed up the responder chain, giving others a chance to respond.
UIResponder defines a number of methods that we can override to respond to touch events. These include touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCanceled:withEvent:. Both our views and our view controllers inherit from UIResponder, so we can implement these methods in either place.
If you want to receive the event and not let it pass on up the responder chain, you must implement all four methods, and you should not call super. If you want to just peek at the event and then place it back on the responder chain (possibly letting others respond to it as well), you can just implement the methods you are interested in—but you must call super somewhere inside your method.
While this lets us easily receive simple touch information, tracking touches over time to detect more-complex gestures gets very complex. Even something as simple as distinguishing between a single tap, a double tap, and a long press would involve a fair bit of code. Worse yet, if developers create their own, custom algorithms to recognize each gesture, different applications may respond slightly differently to different gestures.
To solve both of these problems, the iOS SDK provides a set of predefined gesture recognizers. Each gesture recognizer has a number of properties and methods and can be used either to request information from the recognizer or to fine-tune the recognizer’s behavior. The recognizers are listed in Table 4.2. We can also create our own, custom gesture recognizers—though that is an advanced topic and beyond the scope of this book.
TABLE 4.2 Gesture Recognizers
Gesture Recognizer |
Description |
UITapGestureRecognizer |
Triggers an action once the tap gesture is recognized. You can set both the number of fingers and the number of taps, letting you distinguish between, say, a one-finger tap and a four-finger quadruple-tap. |
UIPinchGestureRecognizer |
Returns continuous pinch data. This is typically used to modify something’s size. |
UIRotationGestureRecognizer |
Returns continuous rotation data. This is typically used to rotate objects. |
UISwipeGestureRecognizer |
Triggers an action when the swipe gesture is recognized. You can set the direction and the number of fingers for the gesture. |
UIPanGestureRecognizer |
Returns continuous location data as the finger is moved around the screen. You can set the minimum and maximum number of fingers. |
UIScreenEdgePanGestureRecognizer |
Like the pan gesture, but it recognizes only the gestures that originate from the edge of the screen. This gesture is new with iOS 7. We can set the edges that the gesture will monitor. |
UILongPressGestureRecognizer |
Triggers an action when the user presses and holds a finger on the screen. We can set the duration, the number of fingers, the number of taps before the hold, and the amount of motion allowed. While this is primarily used to trigger actions, it will return continuous data as well. |
In many cases, we can set up our gesture recognizers in Interface Builder. We can drag them out and drop them onto a view. Then we can select the gesture recognizer icon to set its attributes. Finally, we can Control-drag from the icon to our code in the Assistant editor to create a method that will be called whenever the recognizer fires.
For discrete gesture recognizers, this method is called once, when the gesture is recognized. For continuous recognizers, it is called when the gesture begins, again for each update, and then when the gesture is canceled or ends. Our method will need to check the recognizer’s state and respond accordingly.
We’re going to add screen edge pan recognizers to all the view controllers managed by our tab bar. This is a continuous gesture, so we can monitor how far the user has moved their finger across the screen.
We could do this in Interface Builder; however, the screen edge pan recognizer has not been added to the library yet. So, we cannot just drag and drop it in place. More importantly, we will want to automate adding these gesture recognizers based on the tab’s index; that way the gesture recognizers will be added correctly even if we end up adding or removing tabs later.
Open RootTabBarController.m. Add the following method to the bottom of the private methods:
- (void
)addPanGestureRecognizerToViewController: (UIViewController
*)controller forEdges:(UIRectEdge
)edges {NSParameterAssert
((edges ==UIRectEdgeLeft
)||(edges ==
UIRectEdgeRight
));SEL
selector;if
(edges ==UIRectEdgeLeft
) { selector =@selector
(handleRightPan:); }else
{ selector =@selector
(handleLeftPan:); }UIScreenEdgePanGestureRecognizer
*panRecognizer = [[UIScreenEdgePanGestureRecognizer
alloc
]initWithTarget
:self
.interactiveTransition
action
:selector]; panRecognizer.maximumNumberOfTouches
=1
; panRecognizer.minimumNumberOfTouches
=1
; panRecognizer.edges
= edges; [controller.view
addGestureRecognizer
:panRecognizer]; }
Here we start with a parameter assert. This is just like a normal NSAssert, but it’s specifically designed for checking incoming parameter values. It’s also easier to use, since the system automatically generates the error message for us. In this case, we just want to verify that we are setting our recognizer to watch either the left or right edge (not the top, bottom, or multiple edges).
Next, we determine the selector for the method that should be called when the gesture is triggered. If we’re panning from the left to the right, it’s the handleRightPan: method. If we’re panning from the right to the left, it’s the handleLeftPan: method. The right or left in the handler’s name refers to the direction of motion—not the starting position.
We then create our gesture recognizer, passing in our interactive transition as the target, and our selector as the action. When the recognizer is triggered, it will call the specified method on our interactive transition object.
Finally, we set it to respond to one, and only one finger, from the specified edge. Then we add it to our container’s view.
Now we need to call this method once for each view controller. Add the following method right before addPanGestureRecognzierToViewController:forEdges::
- (void
)setupEdgePanGestureRecognizers {NSUInteger
count = [self
.viewControllers
count
];for
(NSUInteger
index =0
; index < count; index++) {UIViewController
*controller =self
.viewControllers
[index];if
(index ==0
) { [self
addPanGestureRecognizerToViewController
:controllerforEdges
:UIRectEdgeRight
]; }else if
(index == count -1
) { [self
addPanGestureRecognizerToViewController
:controllerforEdges
:UIRectEdgeLeft]
; }else
{ [self
addPanGestureRecognizerToViewController
:controllerforEdges
:UIRectEdgeRight
]; [self
addPanGestureRecognizerToViewController
:controllerforEdges
:UIRectEdgeLeft
]; } } }
This method iterates over all our view controllers. This time, we don’t use fast enumeration, since we actually need the index number. If it’s the first tab, we add a gesture recognizer on the right edge. If it’s the last tab, we add a gesture recognizer on the left edge. If it’s somewhere in the middle, we add them to both edges.
Now, add the following line to the bottom of viewDidLoad:
[self
setupEdgePanGestureRecognizers
];
This sets up our gesture recognizers when our tab bar controller loads.
Now we just need to implement handleRightPan: and handleLeftPan:. Switch to TabInteractiveTransition.m and add the following method to the bottom of the @implementation block:
- (void
)handleLeftPan:(UIPanGestureRecognizer
*)recognizer {CGPoint
translation = [recognizertranslationInView
:self
.parent
.view
];CGFloat
percent =-translation.
x
/CGRectGetWidth
(self
.parent
.view
.bounds
); percent =MAX(percent
,0.0f
); percent =MIN(percent
,1.0f
);if
(recognizer.state
==UIGestureRecognizerStateBegan
) {NSAssert
(self
.oldIndex
==0
,@”We shouldn’t already have an “
@”old index value”
);NSAssert
(self
.newIndex
==0
,@”We shouldn’t already have a “
@”new index value”
);self
.oldIndex
=self
.parent
.selectedIndex
;NSAssert
(self
.oldIndex
!=NSNotFound
,@”Interactive transitions from the “
@”More tab are not possible”
);self
.newIndex
=self
.oldIndex
+1
;NSAssert
(self
.newIndex
< [self
.parent
.viewControllers
count
],@”Trying to navigate past the last tab”
); } [self
handleRecognizer
:recognizerforTransitionPercent
:percent]; }
We start by getting the translation from our gesture recognizer. This is the amount our finger has moved from the original point of contact. We then convert this to a percentage. As a safety step, we clamp this value so that it cannot be lower than 0.0 or higher than 1.0. These values will make sense only during change notifications—not when the recognizer began, ended, or was canceled—but we’re going to precalculate them anyway, making the rest of our code easer to read. If it’s not a change notification, the percent will simply get ignored.
Next, if this is a begin notification, we save our new and old index. Then we call handleRecognzier:forTransitionPercent:.
handleRightPan: is largely the same. The math for calculating our percentage is a little different, since the gesture is moving in the opposite direction.
- (void
)handleRightPan:(UIPanGestureRecognizer
*)recognizer {CGPoint
translation = [recognizertranslationInView
:self
.parent
.view
];CGFloat
percent =translation.
x
/CGRectGetWidth
(self
.parent
.view
.bounds
); percent =MAX(percent
,0.0f
); percent =MIN(percent
,1.0f
);if
(recognizer.state
==UIGestureRecognizerStateBegan
) {NSAssert
(self
.oldIndex
==0
,@”We shouldn’t already have an “
@”old index value”
);NSAssert
(self
.newIndex
==0
,@”We shouldn’t already have a “
@”new index value”
);self
.oldIndex
=self
.parent
.selectedIndex
;NSAssert
(self
.oldIndex
!=NSNotFound
,@”Interactive transitions from the “
@”More tab are not possible”
);self
.newIndex
=self
.oldIndex
-1
;NSAssert
(self
.newIndex
>=0
,@”Trying to navigate past the first tab”
); } [self
handleRecognizer
:recognizerforTransitionPercent
:percent]; }
Finally, add the handleRecognzier:forTransitionPercent: method after the other two.
- (void
)handleRecognizer:(UIPanGestureRecognizer
*)recognizer forTransitionPercent:(CGFloat
)percent {switch
(recognizer.state
) {case
UIGestureRecognizerStateBegan
:self
.interactive
=YES
;self
.parent
.selectedIndex
=self
.newIndex
;break
;case
UIGestureRecognizerStateChanged
: [self
updateInteractiveTransition
:percent];break
;case
UIGestureRecognizerStateCancelled
:self
.completionSpeed
=0.5
; [self
cancelInteractiveTransition
];self
.interactive
=NO
;self
.newIndex
=0
;self
.oldIndex
=0
;break
;case
UIGestureRecognizerStateEnded
:self
.completionSpeed
=0.5
;if
(percent >0.5
) { [self
finishInteractiveTransition
]; }else
{ [self
cancelInteractiveTransition
]; }self
.newIndex
=0
;self
.oldIndex
=0
;self
.interactive
=NO
;break
;default
:NSLog
(@”*** Invalid state found %@ ***”
,@(recognizer
.state
)
); } }
Since the edge pan recognizer is a continuous recognizer, it will send us updates as our finger moves across the screen. This method checks the recognizer’s state and responds appropriately.
If the recognizer has just begun, it places us in interactive mode and then changes the tab bar’s index to the new index.
Every time we get a change update, it updates our animation percentage by calling UIPercentDrivenInteractiveTransition’s updateInteractiveTransition: method.
If the recognizer is canceled, we cancel the transition, clear our index properties, and set interactive to NO.
Finally, if the gesture ends normally (because the user lifts his finger or runs past the edge of the screen), it checks to see how much we have completed. If it is greater than 50 percent, it finishes the transition. The animation will automatically complete. If it is less than 50 percent, it cancels the transition. The animation will automatically roll back to the initial state.
That’s it. Run the app. You can now swipe your finger from the edge of the screen to trigger the transition.