视图控制器之间的交互过渡

时间:2013-03-21 09:39:21

标签: ios cocoa-touch uiviewcontroller

使用UIView动画API和视图控制器包含,当前的Cocoa Touch堆栈非常适合视图控制器之间的自动转换。

我觉得很难写的是视图控制器之间的交互式转换。例如,当我只想使用推送动画将一个视图替换为另一个视图时,我可以使用UINavigationController或使用包含API并自己编写转换。但是我希望转换是交互式的,触摸控制的很常见:用户开始拖动当前视图,从侧面显示传入视图,并且通过平移触摸手势控制转换。用户可以稍微平移以“窥视”传入视图,然后向后平移,保持当前视图可见。如果手势结束低于某个阈值,则转换将被取消,否则它将完成。

(如果这个不够清楚,我说的是像iBooks中的翻页,但是在不同的视图控制器之间,并推广到任何这样的交互式过渡。)

我知道如何编写这样的转换,但是当前的视图控制器必须对转换了解太多 - 它占用了过多的代码。而且甚至没有提到可以轻松地实现两种不同的交互式转换,在这种情况下,所讨论的控制器充满了与其紧密耦合的转换代码。

是否有抽象的模式,概括交互式转换代码并将其移动到单独的类或代码块中?也许是图书馆,甚至?

3 个答案:

答案 0 :(得分:2)

我不知道有任何库可以做到这一点,但是我已经用UIViewController上的类别抽象出了转换代码,或者为我的视图控制器创建了具有转换代码的基类在里面。我在基类中保留了所有混乱的转换代码,在我的控制器中,我只需要添加一个手势识别器并从其action方法中调用基类方法。

-(IBAction)dragInController:(UIPanGestureRecognizer *)sender {
    [self dragController:[self.storyboard instantiateViewControllerWithIdentifier:@"GenericVC"] sender:sender];
}

编辑后:

这是我的一次尝试。这是DragIntoBaseVC中的代码,它是另一个控制器需要继承的控制器,使用上面的代码将视图拖入其中。这只处理拖拽(仅从右侧),而不是拖拽(仍在处理拖拽,以及如何使这个方向相对于方向更通用)。很多代码都在那里处理轮换。它适用于任何方向(颠倒除外),适用于iPhone和iPad。我通过动画布局约束而不是设置框架来制作动画,因为这似乎是苹果公司的方向(我怀疑他们将来会贬低旧的支柱和弹簧系统)。

#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;
    }];
}

答案 1 :(得分:2)

我觉得有点奇怪试图回答这个问题...就像我只是不理解这个问题一样,因为你无疑比我更了解这一点,但是这里有了。

您使用了包含API并自行编写了转换,但您对结果不满意?到目前为止,我发现它非常有效。我创建了一个没有视图内容的自定义容器视图控制器(将子视图设置为全屏)。我将其设置为rootViewController

我的收容夹视图控制器有一堆预先固定的转换(在enum中指定),每个转换都有预先确定的手势来控制转换。我使用2个手指平移左/右幻灯片,3个手指捏/缩放以进行增长/缩小到屏幕效果的中间和其他几个。有一种设置方法:

- (void)addTransitionTo:(UIViewController *)viewController withTransitionType:(TransitionType)type;

然后我调用方法来设置视图控制器交换输出。

[self.parentViewController addTransitionTo:nextViewController withTransitionType:TransitionTypeSlideLeft];
[self.parentViewController addTransitionTo:previousViewController withTransitionType:TransitionTypeSlideRight];
[self.parentViewController addTransitionTo:infoViewController withTransitionType:TransitionTypeSlideZoom];

父容器为转换类型添加适当的转换手势,并管理视图控制器之间的交互式移动。如果您正在平移并且在中间放开,它将会反弹到覆盖屏幕大部分区域的任何一个。完整转换完成后,容器视图控制器将删除旧视图控制器以及随之而来的所有转换。您还可以使用以下方法随时删除转场:

- (void)removeTransitionForType:(TransitionType)type;

虽然交互式转换很好,但在某些情况下我也想要非交互式转换。我使用了不同的类型,因为我确实有一些只是静态的过渡,因为我不知道哪种手势适合于它们是交互式的(如交叉淡入淡出)。

- (void)transitionTo:(UIViewController *) withStaticTransitionType:(StaticTransitionType)type;

我最初为幻灯片应用程序编写了容器,但从那以后我转过身来在几个应用程序中重复使用它。我还没有将它拉出来重新使用,但这可能只是时间问题。

答案 2 :(得分:2)

这是我到过的API。它有三个组件 - 想要创建到另一个的转换的常规视图控制器,自定义容器视图控制器和转换类。过渡类看起来像这样:

@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

从这个抽象类中,我推导出推动,旋转等具体过渡。大多数工作都在容器控制器中完成(简化了一点):

@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

为了使事情更清楚,这就是过渡的开始:

- (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];
}

TZViewController只是一个简单的UIViewController子类,它包含一个指向转换控制器的指针(非常类似于navigationController属性)。我使用类似于UIPanGestureRecognizer的自定义手势识别器来驱动转换,这是视图控制器中手势回调代码的外观:

- (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;
    }
}

我对结果很满意 - 它非常简单,不使用黑客,很容易扩展新的转换,视图控制器中的代码相当短而且简单。我唯一的抱怨是我必须使用自定义容器控制器,所以我不确定它如何与标准容器和模态控制器一起使用。

相关问题