4

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

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

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

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

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

4

3 回答 3

2

我不知道有任何库可以做到这一点,但我已经用 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;
    }];
}
于 2013-03-26T07:03:36.247 回答
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;

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

于 2013-03-26T19:23:07.513 回答
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;
    }
}

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

于 2013-03-27T15:11:18.853 回答