With the UIView
animation API and view controller containment the current Cocoa Touch stack is very well suited for automatic transitions between view controllers.<
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.