视图控制器之间的交互转换?

Posted

技术标签:

【中文标题】视图控制器之间的交互转换?【英文标题】:Interactive transitions between view controllers? 【发布时间】:2013-03-21 09:39:20 【问题描述】:

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

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

(如果这还不够清楚,我说的是 iBooks 中的翻页,但在不同的视图控制器之间,并泛化到任何此类交互式转换。)

我知道如何编写这样的过渡,但是当前视图控制器必须对过渡了解太多——它占用了太多的代码。更不用说很容易有两种不同的交互转换,在这种情况下,所讨论的控制器充满了转换代码,与它紧密耦合。

是否有一种模式可以抽象、概括交互式转换代码并将其移动到单独的类或代码块中?甚至可能是图书馆?

【问题讨论】:

ios 7 中,有一组用于视图控制器转换的新 API。你可以在这个 github 项目中找到一些交互式转换的例子:github.com/ColinEberhardt/VCTransitionsLibrary @ColinE 干得好!!你的那个 github 项目解决了我所有的疑惑 :) 谢谢 【参考方案1】:

我不知道有任何库可以做到这一点,但我已经抽象出转换代码,或者使用 UIViewController 上的一个类别,或者为我的视图控制器创建一个基类,其中包含转换代码。我将所有杂乱的过渡代码保留在基类中,而在我的控制器中,我只需要添加一个手势识别器并从其动作方法中调用基类方法即可。

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

编辑后:

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

#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,我已经尝试了多种方法,但通常,我将新视图直接添加到窗口的层次结构中(两个视图都是兄弟,你可以决定哪个在顶部)-这样,当转换结束时,我将窗口的根视图控制器更改为传入控制器,并且旧的控制器被释放。如果我想保留旧的控制器,我会保留一个指向它的指针,如果我想反转这个过程,我会在当前视图下添加它的视图,在一个发现类型的转换中。 谢谢,感谢您的讨论。我不想将视图直接放入窗口层次结构中,因为破坏控制器/视图关系会导致各种奇怪的错误。今天之后看起来我会成功使用自定义控制器容器,如果代码真的有效,我稍后会发布 API 详细信息。 @zoul,将视图直接放入窗口的层次结构不会破坏这种关系,只要您在转换结束时将该控制器作为窗口的根视图控制器(我在完成块)。我也使用自定义容器控制器完成了它,但是您仍然需要在转换期间将新控制器的视图添加到其他视图中——我认为没有任何区别。我稍后也会发布我的一个。 太好了,再次感谢您。我已将获得的 API 发布到单独的答案中。它使用了包含 API,因此它可以更好地配合设备旋转,但我还不需要它,所以我没有尝试。【参考方案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;

我最初是为幻灯片类型的应用程序编写容器,但从那时起我转身并在几个应用程序中重新使用它。我还没有把它拉到图书馆里重复使用,但这可能只是时间问题。

【讨论】:

这与我得到的非常相似,现在将发布我的 API。 @DBD:OT,你为什么reject this change?答案中的代码存在编辑器试图纠正的错别字。 @Duck:仍然是 OT。修改代码块的功能是一个滑坡,通常会导致答案或问题发生根本性的变化(有时代码中的错误会导致成为问题)。因此,对于此类编辑,我尝试在阳离子/拒绝方面出错。我对这种改变持观望态度,但选择拒绝。我希望这会导致人们添加一条评论,该评论将向 OP 提供反馈,或者允许修复一个简单的错误,或者如果发布者没有意识到它是错误的,如果适当的话,可能会解决一些误解。【参考方案3】:

这是我得到的 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;
    

我对结果很满意——它相当简单,没有使用任何 hack,很容易通过新的转换进行扩展,并且视图控制器中的代码相当短且简单。我唯一的抱怨是我必须使用自定义容器控制器,所以我不确定它如何与标准容器和模态控制器一起使用。

【讨论】:

如果没有看到整个代码,我真的无法理解你的部分是如何组合在一起的。我仍在尝试找出将其抽象出来的最佳方法,因此它非常通用且易于放入任何项目中。如果你有可以分享的演示项目,我很想看看。 我有一个可用的静态库,其中包含一个平移手势识别器、几个过渡动画和一个演示目标,但是为了工作而完成了它,我们公司并不完全愿意开源我们的代码。我想我可以在答案中添加更多细节,你知道什么最有帮助吗? 是的,我认为 startPushingViewController: 的实现代码会很有用。 你去。我宁愿把整个东西放在 GitHub 上,也许以后再说。 @zoul 太棒了!我一直在为 Container ViewController 动画及其交互而苦苦挣扎。我希望我能看到这一点,我一直在努力解决包含 API 和 iOS7 的问题。

以上是关于视图控制器之间的交互转换?的主要内容,如果未能解决你的问题,请参考以下文章

自定义交互过渡动画

管理容器视图控制器内同级视图控制器之间的交互

隐藏状态栏的视图控制器的交互式视图控制器转换

使用平移手势识别器关闭视图控制器

如何让 UIKit 为我提供自定义转换的转换协调器?

自定义交互式演示转换