视图控制器之间如何实现交互式转换?

4
使用UIView动画API和视图控制器容器,当前的Cocoa Touch栈非常适合于视图控制器之间的自动转换。
我发现难以编写的是视图控制器之间的交互式转换。例如,当我只想使用推送动画将一个视图替换为另一个视图时,我可以使用UINavigationController或使用容器API并自己编写转换。但很常见的情况是,我希望转换是交互式的、通过触摸控制的:用户开始拖动当前视图,即将进入的视图从侧面出现,并且过渡由平移触摸手势控制。用户可以稍微拖动一下来“窥视”即将进入的视图,然后再拖回去,保持当前视图可见。如果手势在某个阈值以下结束,则转换被取消,否则转换完成。
(如果这不够清楚,我的意思是类似于iBooks中的翻页效果,但是在不同的视图控制器之间,并推广到任何此类交互式转换。)
我知道如何编写这样的转换,但当前的视图控制器必须对转换了解得太多——它占用了太多的代码。更不用说可能有两种不同的交互式转换,涉及的控制器充满了转换代码,与之紧密耦合。
是否有一种模式可以将交互式转换代码抽象、概括,并将其移入单独的类或代码块中?甚至是一个库?

3
在iOS 7中,新增了一组用于视图控制器转换的API。您可以在以下GitHub项目中找到一些交互式转换的示例:https://github.com/ColinEberhardt/VCTransitionsLibrary。 - ColinE
@ColinE 做得好!!你的 Github 项目解决了我所有的疑问 :) 谢谢 - Vinayak Kini
3个回答

2

我不知道有哪些库可以实现这个功能,但我通过创建UIViewController类别或制作视图控制器的基类来抽象出过渡代码。我将所有混乱的过渡代码放在基类中,在我的控制器中,我只需要添加手势识别器并从其操作方法调用基类方法。

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

谢谢!我已经开始抽象化转换代码,但是我正在与视图/控制器层次结构作斗争。您如何在出站视图和入站视图之间执行转换?两个视图都必须是同一视图层次结构的一部分。您是否将入站视图插入当前控制器中?(这对于转换非常不方便。)还是找到一个公共父视图控制器并在那里动画过渡?我采取了这种方法,但仍然很混乱。 - zoul
@zoul,我已经尝试了多种方法,但通常我会直接将新视图添加到窗口的层次结构中(这样两个视图就是兄弟姐妹,您可以决定哪个在顶部)--这样,当过渡结束时,我将更改窗口的根视图控制器为传入的控制器,并且旧控制器将被释放。如果我想保留旧控制器,我会保留一个指向它的指针,并在需要反转过程(例如揭示式过渡)时将其视图添加到当前视图下方。 - rdelmar
谢谢,我很感激这次讨论。我不想直接将视图放入窗口层级结构中,因为打破控制器/视图关系会导致各种奇怪的错误。今天之后,看起来我将通过自定义控制器容器成功,如果代码真正有效,我稍后会发布API细节。 - zoul
@zoul,直接将视图放入窗口层次结构中并不会破坏这种关系,只要在过渡结束时将该控制器设置为窗口的根视图控制器(我在完成块中执行此操作)。我也使用自定义容器控制器进行了尝试,但是在过渡期间仍然需要将新控制器的视图添加到其他视图中--我认为没有任何区别。稍后我也会发布我的一个。 - rdelmar
非常好,再次感谢您。我已经将我得到的API发布到了一个单独的答案中。它使用了包含API,因此在设备旋转方面可能更加流畅,但我目前不需要这个功能,所以我没有尝试。 - zoul

2

我有点不知道如何回答这个问题...好像我没有理解到问题,因为您肯定比我更了解这个问题,但是我还是试着回答一下。

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

我的contaiment视图控制器具有一些预定义的转场效果(在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;

我最初编写容器的目的是为了制作幻灯片应用程序,但后来我在几个应用程序中重新使用了它。我还没有将其提取到库中以便重复使用,但这只是时间问题。


这与我的结果非常相似,我现在会发布我的API。 - zoul
@DBD:OT,你为什么拒绝了这个更改?答案中的代码有错别字,编辑试图进行更正。 - Mooing Duck
@Duck:还是有点跑题了。修改代码块的功能是一条很容易滑向基本上改变答案或问题的斜坡(有时代码中的错误实际上就是问题)。因此,我尽量在这些编辑方面保持谨慎/拒绝的态度。对于那个更改,我犹豫不决,但最终选择了拒绝。我的希望是这会促使该人添加评论,为原始发布者提供反馈,以便修复简单的错误或者如果适当的话,解决一些误解。 - DBD

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

我对结果感到满意 - 它相当简单,不使用任何黑科技,易于通过新的转换进行扩展,并且视图控制器中的代码相对较短和简单。我唯一的抱怨是我必须使用自定义容器控制器,所以我不确定它如何与标准容器和模态控制器配合使用。


如果没有看到整个代码,我真的无法理解你的代码片段是如何组合在一起的。我仍在努力找出最好的抽象方法,使其非常通用且易于应用到任何项目中。如果您有可以分享的演示项目,我想看看它。 - rdelmar
我有一个带有平移手势识别器、多个过渡动画和演示目标的工作静态库,但这是为了公司而做的,我们公司并不太热衷于开源我们的代码。我认为我可以在答案中添加更多细节,你有什么想法可以帮助最多? - zoul
是的,我认为startPushingViewController:的实现代码会很有用。 - rdelmar
就是这样。我更愿意把整个东西放在GitHub上,也许以后吧。 - zoul
@zoul 太棒了!我一直在苦苦挣扎容器 ViewController 动画及其交互。我希望我能看到这个实现,一直在苦苦挣扎 containment API 和 iOS7。 - user134611

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接