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

借助UIViewanimationAPI和视图控制器控制,当前的Cocoa Touch栈非常适合视图控制器之间的自动转换。

我发现很难写的是视图控制器之间的交互式转换。 作为一个例子,当我只想用另一个视图replace一个视图时,我可以使用UINavigationController或使用包含API并自己编写转换。 但是,我希望转换是交互式的,触摸控制是很常见的:用户开始拖动当前视图,传入视图从侧面出现,转换由平移触摸手势控制。 用户可以平移一些,以便在传入视图上“偷看”,然后平移,保持当前视图可见。 如果手势结束低于某个阈值,则转换被取消,否则完成。

(如果这还不够清楚,我正在讨论类似于iBooks中的页面转换,但是在不同的视图控制器之间,并且推广到任何这样的交互转换。

我知道如何编写这样的转换,但是当前的视图控制器必须知道转换的过多 – 它占用了太多的代码。 而且这甚至没有提到可能容易出现两种不同的交互式转换,在这种情况下,所讨论的控制器充满了转换代码,与之紧密相连。

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

我不知道有任何库可以这样做,但是我已经使用UIViewController上的一个类别提取了转换代码,或者为我的视图控制器创build了一个具有转换代码的基类。 我将基本类中所有乱七八糟的转换代码保留下来,在我的控制器中,我只需要添加一个手势识别器,并从其操作方法中调用基类方法。

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

编辑后:

这是我的一个尝试。 这是DragIntoBaseVC中的代码,它是另一个控制器需要inheritance的控制器,使用上面的代码将视图拖入其中。 这只能处理(从右边)拖动,而不是拖出(仍然在这个工作,以及如何使这个方向相对于通用)。 很多这个代码在那里处理旋转。 它可以在任何方向(除了颠倒)工作,并可以在iPhone和iPad上使用。 因为这似乎是苹果公司正在前进的方式(我怀疑他们会在未来贬值旧的支柱和弹簧系统),所以我正在通过animation布局约束而不是设置框架来进行animation制作。

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

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

您已经使用了遏制API并自己写了过渡,但是对结果不满意? 到目前为止,我发现它非常有效。 我创build了一个没有视图内容的自定义容器视图控制器(将子视图设置为全屏)。 我把它设置为我的rootViewController

我的包容视图控制器有一堆预先固定的转换(在一个enum指定),每个转换具有预定的手势来控制转换。 我使用左右两个幻灯片的两个手指平移,三个手指捏/缩放到屏幕效果的中间增长/缩小等等。 有一种方法来设置:

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

父容器为转换types添加适当的转换手势并pipe理视图控制器之间的交互式移动。 如果你正在平移,而你放开中间,则会反弹回覆盖大部分屏幕的人。 完整转换完成后,容器视图控制器将删除旧的视图控制器以及与之相关的所有转换。 您也可以随时使用此方法删除转场:

 - (void)removeTransitionForType:(TransitionType)type; 

虽然交互式转换很好,但是也有一些情况我也想要非交互式转换。 我使用了不同的types,因为我确实有一些只是静态的转换,因为我不知道什么样的手势适合他们进行交互(比如淡入淡出)。

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

我最初写了一个幻灯片的应用程序的容器,但我已经转过身,从那以后在一些应用程序重新使用它。 我还没有把它拉出到一个图书馆重新使用,但这可能只是一个时间问题。

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

 @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的自定义手势识别器来驱动转换,这是视图控制器中的手势callback代码的外观:

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

我对结果感到满意 – 这相当简单,不使用黑客,很容易通过新的转换进行扩展,视图控制器中的代码相当简短。 我唯一的抱怨是,我必须使用自定义容器控制器,所以我不知道如何玩标准容器和模式控制器。