Interactive transitions between view controllers?
cocoa-touch, ios, uiviewcontroller
Solution
This is the API I have arrived at. There are three components to it – the regular view controller that wants to create a transition to another one, a custom container view controller, and a transition class. The transition class looks like this:
@interface TZInteractiveTransition : NSObject
@property(strong) UIView *fromView;
@property(strong) UIView *toView;
// Usually 0–1 where 0 = just fromView visible and 1 = just toView visible
@property(assign, nonatomic) CGFloat phase;
// YES when the transition is taken far enough to perform the controller switch
@property(assign, readonly, getter = isCommitted) BOOL committed;
- (void) prepareToRun;
- (void) cleanup;
@end
From this abstract class I derive the concrete transitions for pushing, rotating etc. Most work is done in the container controller (simplified a bit):
@interface TZTransitionController : UIViewController
@property(strong, readonly) TZInteractiveTransition *transition;
- (void) startPushingViewController: (TZViewController*) controller withTransition: (TZInteractiveTransition*) transition;
- (void) startPoppingViewControllerWithTransition: (TZInteractiveTransition*) transition;
// This method finishes the transition either to phase = 1 (if committed),
// or to 0 (if cancelled). I use my own helper animation class to step
// through the phase values with a nice easing curve.
- (void) endTransitionWithCompletion: (dispatch_block_t) completion;
@end
To make things a bit more clear, this is how the transition starts:
- (void) startPushingViewController: (TZViewController*) controller withTransition: (TZInteractiveTransition*) transition
{
NSParameterAssert(controller != nil);
NSParameterAssert([controller parentViewController] == nil);
// 1. Add the new controller as a child using the containment API.
// 2. Add the new controller’s view to [self view].
// 3. Setup the transition:
[self setTransition:transition];
[_transition setFromView:[_currentViewController view]];
[_transition setToView:[controller view]];
[_transition prepareToRun];
[_transition setPhase:0];
}
The `TZViewController` is just a simple `UIViewController` subclass that holds a pointer to the transition controller (very much like the `navigationController` property). I use a custom gesture recognizer similar to `UIPanGestureRecognizer` to drive the transition, this is how gesture callback code in the view controller looks:
- (void) handleForwardPanGesture: (TZPanGestureRecognizer*) gesture
{
TZTransitionController *transitionController = [self transitionController];
switch ([gesture state]) {
case UIGestureRecognizerStateBegan:
[transitionController
startPushingViewController:/* build next view controller */
withTransition:[TZCarouselTransition fromRight]];
break;
case UIGestureRecognizerStateChanged: {
CGPoint translation = [gesture translationInView:[self view]];
CGFloat phase = fabsf(translation.x)/CGRectGetWidth([[self view] bounds]);
[[transitionController transition] setPhase:phase];
break;
}
case UIGestureRecognizerStateEnded: {
[transitionController endTransitionWithCompletion:NULL];
break;
}
default:
break;
}
}
I’m happy with the result – it’s fairly straightforward, uses no hacks, it’s easy to extend with new transitions and the code in the view controllers is reasonably short & simple. My only gripe is that I have to use a custom container controller, so I’m not sure how that plays with the standard containers and modal controllers.
Problem
With the `UIView` animation API and view controller containment the current Cocoa Touch stack is very well suited for automatic transitions between view controllers. What I find hard to write are interactive transitions between view controllers. As an example, when I just want to replace one view with another using a push animation, I can use `UINavigationController` or use the containment API and write the transition myself. But it’s quite common that I want the transition to be interactive, touch-controlled: user starts dragging the current view, the incoming view appears from the side and the transition is controlled by the panning touch gesture. The user can just pan a little to “peek” at the incoming view and then pan back, keeping the current view visible. If the gesture ends below a certain treshold, the transition is cancelled, otherwise it’s completed. (In case this is not clear enough, I’m talking about something like the page turn in iBooks, but between different view controllers, and generalized to any such interactive transition.) I know how to write such a transition, but the current view controller has to know too much about the transition – it takes up too much of its code. And that’s not even mentioning that there can easily be two different interactive transitions possible, in which case the controllers in question are full of the transition code, tightly coupled to it. Is there a pattern to abstract, generalize the interactive transition code and move it into a separate class or code lump? Maybe a library, even?