With the UIView
animation API and view controller containment the current Cocoa Touch stack is very well suited for automatic transitions between view controllers.<
I don't know of any libraries to do this, but I've abstracted out the transition code with either a category on UIViewController, or by making a base class for my view controllers that has the transition code in it. I keep all the messy transition code in the base class, and in my controller, I just need to add a gesture recognizer and call the base class method from its action method.
-(IBAction)dragInController:(UIPanGestureRecognizer *)sender {
[self dragController:[self.storyboard instantiateViewControllerWithIdentifier:@"GenericVC"] sender:sender];
}
After Edit:
Here is one of my attempts. This is the code in the DragIntoBaseVC which is the controller that another controller needs to inherit from to have a view dragged into it using the above code. This only handles the drag in (from the right only), not the drag out (still working on that one, and how to make this one more generic with respect to direction). A lot of this code is in there to handle rotations. It works in any orientation (except upside down), and works on both iPhone and iPad. I'm doing the animations by animating the layout constraints rather than setting frames, since this seems to be the way Apple is heading (I suspect they'll depreciate the old struts and springs system in the future).
#import "DragIntoBaseVC.h"
@interface DragIntoBaseVC ()
@property (strong,nonatomic) NSLayoutConstraint *leftCon;
@property (strong,nonatomic) UIViewController *incomingVC;
@property (nonatomic) NSInteger w;
@end
@implementation DragIntoBaseVC
static int first = 1;
-(void)dragController:(UIViewController *) incomingVC sender:(UIPanGestureRecognizer *) sender {
if (first) {
self.incomingVC = incomingVC;
UIView *inView = incomingVC.view;
[inView setTranslatesAutoresizingMaskIntoConstraints:NO];
inView.transform = self.view.transform;
[self.view.window addSubview:inView];
self.w = self.view.bounds.size.width;
NSLayoutConstraint *con2;
switch ([UIDevice currentDevice].orientation) {
case 0:
case 1:
self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeLeft relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.w];
con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeTop relatedBy:0 toItem:inView attribute:NSLayoutAttributeTop multiplier:1 constant:0];
break;
case 3:
self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeBottom relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:self.w];
con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeLeft relatedBy:0 toItem:inView attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
break;
case 4:
self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:-self.w];
con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeRight relatedBy:0 toItem:inView attribute:NSLayoutAttributeRight multiplier:1 constant:0];
break;
default:
break;
}
NSLayoutConstraint *con3 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeWidth relatedBy:0 toItem:inView attribute:NSLayoutAttributeWidth multiplier:1 constant:0];
NSLayoutConstraint *con4 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:0 toItem:inView attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
NSArray *constraints = @[self.leftCon,con2,con3,con4];
[self.view.window addConstraints:constraints];
first = 0;
}
CGPoint translate = [sender translationInView:self.view];
if ([UIDevice currentDevice].orientation == 0 || [UIDevice currentDevice].orientation == 1 || [UIDevice currentDevice].orientation == 3) { // for portrait or landscapeRight
if (sender.state == UIGestureRecognizerStateBegan || sender.state == UIGestureRecognizerStateChanged) {
self.leftCon.constant += translate.x;
[sender setTranslation:CGPointZero inView:self.view];
}else if (sender.state == UIGestureRecognizerStateEnded){
if (self.leftCon.constant < self.w/2) {
[self.view removeGestureRecognizer:sender];
[self finishTransition];
}else{
[self abortTransition:1];
}
}
}else{ // for landscapeLeft
if (sender.state == UIGestureRecognizerStateBegan || sender.state == UIGestureRecognizerStateChanged) {
self.leftCon.constant -= translate.x;
[sender setTranslation:CGPointZero inView:self.view];
}else if (sender.state == UIGestureRecognizerStateEnded){
if (-self.leftCon.constant < self.w/2) {
[self.view removeGestureRecognizer:sender];
[self finishTransition];
}else{
[self abortTransition:-1];
}
}
}
}
-(void)finishTransition {
self.leftCon.constant = 0;
[UIView animateWithDuration:.3 animations:^{
[self.view.window layoutSubviews];
} completion:^(BOOL finished) {
self.view.window.rootViewController = self.incomingVC;
}];
}
-(void)abortTransition:(int) sign {
self.leftCon.constant = self.w * sign;
[UIView animateWithDuration:.3 animations:^{
[self.view.window layoutSubviews];
} completion:^(BOOL finished) {
[self.incomingVC.view removeFromSuperview]; // this line and the next reset the system back to the inital state.
first = 1;
}];
}
I feel a bit odd trying to answer this... like I'm just not understanding the question, because you no doubt know this better than I do, but here goes.
You've using the containment API and written the transition yourself, but your unhappy with the result? I've found it very effective so far. I've created a custom container view controller with no view content(set the child view to be full screen). I set this as my rootViewController
.
My containment view controller comes has a bunch of pre-canned transitions (specified in an enum
) and each transition has pre-determined gesture to control the transition. I use 2 finger panning for left/right slides, 3 finger pinch/zoom for sort of grow/shrink to the middle of the screen effect and a few others. There is a method to setup:
- (void)addTransitionTo:(UIViewController *)viewController withTransitionType:(TransitionType)type;
I then call methods to setup my view controller swap outs.
[self.parentViewController addTransitionTo:nextViewController withTransitionType:TransitionTypeSlideLeft];
[self.parentViewController addTransitionTo:previousViewController withTransitionType:TransitionTypeSlideRight];
[self.parentViewController addTransitionTo:infoViewController withTransitionType:TransitionTypeSlideZoom];
The parent container adds in the appropriate transition gestures for the transition type and manages the interactive movement between the view controllers. If you are panning and you let go in the middle, it will bounce back to whichever one was covering the majority of the screen. When a full transition is complete, the container view controller removes the old view controller and all the the transitions which went with it. You can also remove transitions anytime with the method:
- (void)removeTransitionForType:(TransitionType)type;
While the interactive transitions are nice, there are some cases when I do want non-interactive transitions too. I use a different type for that, because I do have some transitions which are only static because I have no clue what gesture would be appropriate for them to be interactive (like cross fade).
- (void)transitionTo:(UIViewController *) withStaticTransitionType:(StaticTransitionType)type;
I originally wrote the container for a slide deck sort of app, but I've turned around and re-used it in a couple apps since then. I have not yet pulled it out into a library to re-use, but it's probably just a matter of time.
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.