4

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?

zoul
  • 102,279
  • 44
  • 260
  • 354
  • 3
    With iOS 7 there is a new set of APIs for view controller transitions. You can find some examples of interactive transitions in this github project: https://github.com/ColinEberhardt/VCTransitionsLibrary – ColinE Sep 20 '13 at 05:26
  • @ColinE Great work!! That github project of yours resolved all my doubts :) Thanks – Vinayak Kini Oct 17 '13 at 07:12

3 Answers3

2

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;
    }];
}
rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • Thanks! I have started to abstract the transition code, but I’m fighting with the view/controller hierarchy. How do you perform the transition between the outgoing and the incoming view? Both views have to be a part of the same view hierarchy. Do you insert the incoming view into the current controller? (That’s very inconvenient for transformations.) Or do you find a common parent view controller and animated the transition there? I took that approach, but it’s still messy. – zoul Mar 26 '13 at 09:05
  • @zoul, I've tried it multiple ways, but usually, I add the new view directly to the window's hierarchy (both views are then siblings, and you can decide which is on top) -- that way, when the transition is over, I change the window's root view controller to the incoming controller, and the old one is deallocated. If I want to keep the old controller, I'll keep a pointer to it, and add its view underneath the current view if I want to reverse the process, in an uncover type of transition. – rdelmar Mar 26 '13 at 15:35
  • Thanks, I appreciate the discussion. I didn’t want to put the view directly into the window hierarchy, since breaking the controller/view relationship leads to all sorts of weird bugs. After today it looks like I will succeed with a custom controller container, I’ll post the API details later if the code really works. – zoul Mar 26 '13 at 16:51
  • @zoul, putting the view into the window's hierarchy directly doesn't break that relationship, as long as you make that controller the window's root view controller at the end of the transition (which I do in the completion block). I've done it with custom container controllers as well, but you still need to add the new controller's view to some other view during the transition -- I don't think there's any difference. I'll post one of mine later too. – rdelmar Mar 26 '13 at 16:56
  • Great, thank you once again. I have posted the API I have arrived at into a separate answer. It uses the containment API so it may play better with device rotation, but I don’t need that yet, so I didn’t try. – zoul Mar 27 '13 at 15:22
2

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.

DBD
  • 23,075
  • 12
  • 60
  • 84
  • This is very similar to what I have arrived at, will post my API now. – zoul Mar 27 '13 at 15:00
  • @DBD: OT, why did you [reject this change](http://stackoverflow.com/review/suggested-edits/1791862)? The code in the answer had typos that the editor was attempting to correct. – Mooing Duck Mar 27 '13 at 18:21
  • @Duck: still OT. Modifying functionality of code blocks is a slippery slope which can often lead a fundamentally changed answer or question (sometimes a mistake in the code winds up *being* the question). Because of this I try to error on the side of cation/reject for these sorts of edits. I was on the fence for that change, but choose reject. My hope is this leads the person to add a comment which will provide feedback to the OP either allowing the fix of a simple error or possibly addressing some misunderstanding if the poster did not realize it was in error if appropriate. – DBD Mar 27 '13 at 19:36
2

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.

zoul
  • 102,279
  • 44
  • 260
  • 354
  • I can't really understand how your pieces fit together without seeing the whole code. I'm still trying to figure out the best way to abstract this out so it's very general and easy to drop into any project. If you have a demo project that you can share, I'd like to see it. – rdelmar Mar 27 '13 at 16:22
  • I have a working static library with a panning gesture recognizer, several transition animations, and a demo target, but have done it for work and our company isn’t exactly extatic about opensourcing our code. I think I can add more details to the answer, do you have an idea what would help the most? – zoul Mar 27 '13 at 16:36
  • Yeah, I think the implementation code for startPushingViewController: would be useful to see. – rdelmar Mar 27 '13 at 16:45
  • There you go. I’d so much rather put the whole thing on GitHub, maybe later. – zoul Mar 27 '13 at 16:51
  • @zoul excellent! I've been struggling with Container ViewController animation and their interactions. I wish I could see this in action, been struggling with containment API and iOS7. – user134611 Aug 03 '14 at 07:18