天天品尝iOS7甜点 :: Day 14 :: Interactive View Controller Transitions

这篇文章是天天品尝iOS7甜点系列的一部分,你可以查看完整的系列目录:天天品尝iOS7甜点


Introduction - 介绍

回顾Day 10,我们已经介绍了如何在导航(navigation)视图中创建自定义的平滑转换效果。交互式试图控制器转换添加了另外的维度,允许用户进行交互式控制,通常是手势操作。

今天的文章将会查看如何为一个模态的试图控制器创建一个交互式视图转换,使用一个类似于翻牌的效果。视图的翻拍效果动画,随着用户的手势变化而变化。

本章的实例程序能够在github上面进行访问,访问地址:github.com/ShinobiControls/iOS7-day-by-day

Flip Transition Animation - 翻转过渡效果

交互转换扩大了自定义的动画,因此我们需要自己创建一个自定义的渐变动画。我们需要一个对象适配了UIViewControllerAnimatedTransitioning协议.

1
2
3
@interface SCFlipAnimate: NSObject <UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) BOOL dismissal;
@end

我们定义了一个dismissal属性,它将会用来确定牌的翻转方向。在这之前我们需要实现2个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// Get the respective view controllers
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];


// Get the views
UIView *containerView = [transitionContext containerView];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;

// Add the toView to the container
[containerView addSubview:toView];

// Set the frames
CGRect initialFrame = [transitionContext initialFrameForViewController:fromVC];
fromView.frame = initialFrame;
toView.frame = initialFrame;

// Start building the transform - 3D so need perspective
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1/CGRectGetHeight(initialFrame);
containerView.layer.sublayerTransform = transform;

CGFloat direction = self.dismissal ? -1.0 : 1.0;

toView.layer.transform = CATransform3DMakeRotation(-direction * M_PI_2, 1, 0, 0);
[UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
// First half is rotating in
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
fromView.layer.transform = CATransform3DMakeRotation(direction * M_PI_2, 1, 0, 0);
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
toView.layer.transform = CATransform3DMakeRotation(0, 1, 0, 0);
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}];
}

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 1.0;
}

这个动画的方法看起来十分的复杂,但是它使用新的UIView的关键帧动画,这个我们已经在Day 11中介绍了.重要的一部分需要注意的是dismissal属性,它用来决定旋转方向的执行。其他的是,动画执行部分,这个时候我们并不是深入讨论细节部分。要查看详细的信息Day 10 的custom view controller transitions和Day 11的UIView key-frame animates

现在我们有了一个动画的对象,我们需要把它添加到我们自己的视图控制器转换中,我们已经创建了一个storyboard,它包含了2个视图控制器。第一个包含的按钮,它可以触发展示一个模态视图控制器,第二个包含的按钮,它能够是模态视图控制器消失。

1
2
3
- (IBAction)handleDismissPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:NULL];
}

如果我们运行应用程序,然后我们就可以看到标准的转换动画来展示和消失一个模态的视图控制器。还有一个标准的翻转的转换我们可以使用,但是我们对使用自定义的动画比较的感兴趣,所以让我们来添加自定义的转换动画。

在Day 10中我们控制导航控制器(navigation controller)的转换,所以我们实现了UINavigationControllerDelegated协议。在这里我们需要连接模态视图控制器来控制它的转换,所以我们需要实现UIViewControllerTransitioningDelegate协议。它有我们熟悉的方法,但是我们需要其中的2个方法是animationControllerForPresentedController:presentingController:sourceController:animationControllerForDismissedController:,我们需要在主要的视图控制器中实现这些方法,并且返回我们在上面创建的动画对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface SCViewController () <UIViewControllerTransitioningDelegate> {
SCFlipAnimation *_flipAnimate;
}
@end

@implementation SCViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_flipAnimation = [SCFlipAnimation new];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControlerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
_flipAnimation.dismissal = NO;
return _flipAnimation;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
_flipAnimation.dismissal = YES;
return _flipAnimation;
}

@end

这里需要注意的比较重要的信息是在presentdismiss方法中设置的dismissal属性是不同的,代表了不同的翻转方向。第二需要做的就是进行设置视图控制器的代理:

1
2
3
4
5
6
7
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.destinationViewController isKindOfClass:[SCModalViewController class]]) {
// Set the delegate
SCModalViewController *vc = (SCModalViewController *)segue.destinationViewController;
vc.transitioningDelegate = self;
}
}

如果你现在运行应用程序,你就可以查看到我哦们自定义的动画切换效果了.

Interactive transitioning - 交互式转换

UIViewControllerTransitioningDelegate协议中还有2个方法来用提供交互式转换的支持,他们两个都返回一个实现UIViewControllerInteractiveTransitioning协议的对象。我们可以自己创建对象来实现这些协议,但是Apple已经为我们提供了一些具体类的形式,UIPercentDrivenInteractiveTransition已经包含了绝大多数用例。

交互的概念(实现了UIViewControllerInteractiveTransitioning的对象)就是它可以控制动画效果(实现了UIViewControllerAnimatedTransitioning协议的对象)的过程。UIPercentDrivenInteractiveTransition类提供了一些方法可以用百分比的方式来激活指定的动画过程。

这些添加到我们项目中是十分简单的。我们想要创建一个pan手势,它代表了用户竖直的拖拽,这将会控制模式视图的展现和消失。我们将会创建一个UIPercentDrivenInteractiveTransition的子类,它包含了下面的一些属性:

1
2
3
4
5
6
7
@interface SCFlipAnimationInteractor: UIPercentDrivenInteractiveTransition

@property (nonatomic, strong, readonly) UIPanGestureRecognizer *gestureRecogniser;
@property (nomatomic, assign, readonly) BOOL interactionInProgress;
@property (nonatomic, weak) UIViewController<SCInterctiveTransitionViewControllerDelegate> *presentingVC;

@end

其中的手势就是我们上面刚刚说的实现的那个效果,现在我们需要查看的是需要一个适配简单的协议:

1
2
3
@protocol SCInteractiveTransitionViewControllerDelegate <NSObject>
- (void)proceedToNextViewController;
@end

子类的实现体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface SCFlipAnimationInteractor ()
@property (nonatomic, strong, readwrite) UIPanGestureRecognizer *gestureRecogniser;
@property (nonatomic, strong, readwrite) BOOL interactionInProgress;
@end

@implementation SCFlipAnimationInteractor

- (instancetype)init {
self = [super init];
if (self) {
self.gestureRecogniser = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
}
return self;
}

@end

首先,我们需要重新定义两个属性为可读可写状态,然后设置手势。需要注意的是这是我们并没有把手势和任何的视图关联起来.

手势操作的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)handlePan:(UIPanGestureRecognizer *)pgr {
CGPoint translation = [pgr ranslationInView:pgr.view];
CGFloat percentage = fabs(translation.y / CGRectGetHeight(pgr.view.bounds));
switch (pgr.state) {
case UIGestureRecognizerStateBegan: {
self.interactionInProgress = YES;
[self.presentingVC proceedToNextViewController];
}
break;

case UIGestureRecognizerStateChanged:
[self updateInteractiveTransition:percentage];
break;

case UIGestureRecognizerStateEnded: {
if (percentage < 0.5) {
[self cancelInteractiveTransition];
}else {
[self finishInteractiveTransition];
}
self.interactionInProgress = NO;
}
break;

case UIGestureRecognizerStateCancelled: {
[self cancelInteractiveTransition];
self.interactionInProgress = NO;
}
break;

default:
break;
}
}

上面手势操作就是我们在交互中需要用到的。

首先,我们需要为交互转换添加新的方法,需要在主要的试图控制器中实现UIViewControllerTransitioningDelegate协议:

1
2
3
4
5
6
7
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator {
return _animationInteractor.interactionInProgress ? _animationInteractor : nil;
}

- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
return _animationInteractor.interactionInProgress ? _animationInteractor : nil;
}

这里有两个一直的方法(展示和消失).我们值需要在进行交互转换的时候返回一个交互对象.也就是用户点击按钮而不是手势操作,我们就指定一个非交互式转换。这就是我们交互类中的interactionInProgress属性的作用。我们返回一个_animationInteractor类实例,这个在viewDidLoad方法中进行赋值:

1
2
3
4
5
6
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_animationInteractor = [SCFlipAnimationInteractor new];
_flipAnimation = [SCFlipAnimation new];
}

我们在自定义的交互中创建了手势,但是并没有把它与视图相关联,所以我们现在来做这些工作,在我们的视图控制器中的viewDidAppear:方法中进行设置:

1
2
3
4
5
6
- (void)viewDidAppear:(BOOL)animated {
// Add the gesture recogniser to the window first render time
if (![self.view.window.gestureRecognizers containsObject:_animationInteractor.gestureRecogniser]) {
[self.view.window addGestureRecognizer:_animationInteractor.gestureRecogniser];
}
}

我们很正常的把手势添加到视图中,但是在这里我们却添加到window对象中。这是因为动画发生的时候,视图控制器中的视图将会被移除,因此手势就没有办法响应行为了。把它添加到window上面,将会确保我们希望的行为。如果我们是执行一个导航控制器的转换我们可以把手势添加到导航控制器的视图中。这个手势在viewDidAppear:中添加,因为这个时候window属性已经被附上值了。

最后一个块的困惑是在交互器上面设置presentingVC属性。为了达到这个目的,我们需要确保我们的试图控制器实现了SCInteractiveTransitionViewControllerDelegate协议。在我们的主试图控制器,就变得非常的简单了:

1
2
3
4
5
6
7
8
9
10
@interface SCViewController () <SCInteractiveTransitionViewControllerDelegate, UIViewControllerTransitioningDelegate> {
SCFlipAnimationInteractor *_animationInteractor;
SCFlipAnimation *_flipAnimation;
}
@end

#pragma mark - SCInteractiveTransitionViewControllerDelegate methods
- (void)proceedToNextViewController {
[self performSegueWithIdentifier:@"displayModal" sender:self];
}

到目前为止,我们已经实现了需要的方法,我们可以在交互上面设置正确的属性,具体在viewDidAppear上面设置。这将会确保每一次的展示都是正确的。

1
2
3
4
5
- (void)viewDidAppear:(BOOL)animated {
...
// Set the recipeint of the interactor
_animationInteractor.presentingVC = self;
}

所以,当用户触发拖拽手势的时候,交互器就会调用proceedToNextViewController方法,这个方法可以展示模态视图控制器,这就是全部我们希望得到的。

为了在模态视图控制器上面执行相同的操作,必须要有一个交互器的引用对象(所以,它可以更新presentingVC属性)。

1
2
3
4
5
6
7
@interface SCModalViewController : UIViewController <SCInteractiveTransitionViewControllerDelegate>

...

@property (nonatomic, weak) SCFlipAnimationInteractor *interactor;

@end

我们将会在视图控制器的prepareForSegue:方法中设置这个属性:

1
2
3
4
5
6
7
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.destinationViewController isKindOfClass[SCModalViewController class]]) {
// Set the delegate
...
vc.interactor = _animationInteractor;
}
}
1
2
3
- (void)proceedToNextViewController {
[self dismissViewControllerAnimated:YES completion:NULL];
}

最后,一旦模态视图控制器出现的时候,我们需要更新交互器上面的属性,用来确保下一次交互转换开始(也就是用户开始竖直的拖拽)它将会调用模态视图控制器的方法,而不是最开始的:

1
2
3
4
5
- (void)viewDidAppear:(BOOL)animated {
// Reset which view controller should be the receipient of the
// interactor's transition
self.interactor.presentingVC = self;
}

这就完成了。

Conclusion - 总结

交互式试图控制器的转换是相当的一个复杂的主题,主要的原因是一系列不同的协议需要去实现,也因为代码中没有明确哪一部分负责什么(例如,谁是拥有手势对象的).然而,在实际中,我们使用非常强大的少量的代码。我鼓励你试一试自定义的视图转换,但值得注意的是,具有非常强大的责任,是因为我们现在需要做大量的视图控制器间的转换,我们应该确保我们并没有复杂化用户体验。

本文翻译自:iOS7 Day-by-Day :: Day 14 :: Interactive View Controller Transitions

文章目录
  1. 1. Introduction - 介绍
  2. 2. Flip Transition Animation - 翻转过渡效果
  3. 3. Interactive transitioning - 交互式转换
  4. 4. Conclusion - 总结
,