We Simplify, But We Perfect.


iOS 8 中 UIPresentationController 使用

2016-01-22


这个教程将交你如何制作 iOS 内置的邮件 App 点击新建后弹出一个编辑邮件的 ViewController 的效果,具体的效果如下图所示:

Demo


源码存放在了这里

开始的时候,我们建立两个 UIViewController, 初始界面是一个简单的 FirstViewController, 第二个 SecondViewController 简单的模拟编辑邮件,中间一个 button,点击 dismiss 后运行 dismiss 代码。

首先,明确一个概念,在我们做视图切换的时候(Present 方式)弹出来的新视图叫做 presentedViewController(SecondViewController), 原视图叫做 presentingViewController (FirstViewController), 在做 Present 动画视图切换的时候, 所有的动画效果由 presentedViewController 来控制,也就是即将被弹出的视图控制。让 SecondViewController 实现 UIViewControllerTransitioningDelegate 协议,这个协议指定了动画的弹出方式,以及消失方式等等。

@protocol UIViewControllerTransitioningDelegate <NSObject>

- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

这个协议返回了 UIPresentationController 对象, 这是在 iOS 8 之后新加入的 API, UIPresentationController 是管理视图切换的控制器,它可以控制新出来的视图如何显示,以及它的 size 等等。

我们创建一个 UIPresentationController 的子类 QIUPresentationController,重写 presentationTransitionWillBegin 方法,

- (void)presentationTransitionWillBegin {
	[self.containerView addSubview:self.dimmingView];
	self.dimmingView.frame = self.containerView.bounds;
	self.dimmingView.alpha = 0;
	[self.presentingViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
     self.presentingViewController.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.90, 0.90);
    self.dimmingView.alpha = 0.4;
	} completion:nil];
}

- (UIView *)dimmingView {
	if 	(_dimmingView == nil) {
    	_dimmingView = [[UIView alloc] init];
    	_dimmingView.backgroundColor = [UIColor blackColor];
	}
	return _dimmingView;
}

上述代码就是在弹出新视图后呈现一个向后折叠的视差效果,如果想要 FirstViewController 不全屏显示 ,重写方法 - (CGRect)frameOfPresentedViewInContainerView

- (CGRect)frameOfPresentedViewInContainerView {
	return CGRectMake(0, 100, CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.containerView.bounds) - 100);
}

之后,我们在 SecondViewController 实现 UIViewControllerTransitioningDelegate 的代理方法,并将对象 QIUPresentationController 返回

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
	if (self = [super initWithCoder:aDecoder]) {
    	self.modalPresentationStyle = UIModalPresentationCustom;
    	self.transitioningDelegate = self;
	}
	return self;
}

#pragma mark - <UIViewControllerTransitioningDelegate>

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
    QIUPresentationController *controller = [[QIUPresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    return controller;
}

至此,我们完成了邮件弹入弹出方式的自定义实现。接下来,我们为 SecondViewController 加入手势,当手势滑动超过一定百分比的时候,则向下消失这个 UIViewController。

UIViewControllerTransitioningDelegate 还有一个协议叫做

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed

这个方法就是控制 dismiss 时候的动画效果的,它需要返回一个实现了 UIViewControllerAnimatedTransitioning 的对象, 所以这里, 我们新建一个对象,叫做 QIUDismissAnimation,并且实现协议 UIViewControllerAnimatedTransitioning

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

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        CGRect finalframe = fromView.frame;
        finalframe.origin.y = [transitionContext containerView].bounds.size.height;
        fromView.frame = finalframe;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
	return [[MailDismissAnimation alloc] init];
}

这样,整个 dismiss 的动画过程就被我们自定义了,最后一步去添加手势

- (IBAction)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    CGPoint touchPoinst = [panGesture translationInView:panGesture.view.superview];
    CGFloat percentComplete = (touchPoinst.y - self.startPoint.y) / CGRectGetHeight(panGesture.view.bounds);
    percentComplete = MAX(0, percentComplete);
    percentComplete = MIN(1, percentComplete);
    switch (panGesture.state) {
        case UIGestureRecognizerStateBegan: {
            self.isInteracting = YES;
            self.startPoint = touchPoinst;
            [self dismissViewControllerAnimated:YES completion:nil];
            break;
        }
        case UIGestureRecognizerStateChanged: {
            [self.percentDrivenInteractiveTransition updateInteractiveTransition:percentComplete];
            break;
        }
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateEnded: {
            self.isInteracting = NO;
            if (percentComplete > 0.4) {
                [self.percentDrivenInteractiveTransition finishInteractiveTransition];
            } else {
                [self.percentDrivenInteractiveTransition cancelInteractiveTransition];
            }
        }
            
        default:
            break;
    }
}

补充刚刚 dismiss 的另外一个协议

@property (nonatomic, strong) UIPercentDrivenInteractiveTransition *percentDrivenInteractiveTransition;

- (UIPercentDrivenInteractiveTransition *)percentDrivenInteractiveTransition {
    if (_percentDrivenInteractiveTransition == nil) {
        _percentDrivenInteractiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
    }
	return _percentDrivenInteractiveTransition;
}


- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
    return self.isInteracting ? self.percentDrivenInteractiveTransition : nil;
}

到这里,几乎完成了所有的 mail 效果,但是 其实这不是最完美的解决方案,因为你可以看到当手势拖放一半的时候,后面的那个 CGAffineTransformIdentity 有点跳跃的感觉,更好的解决方案可以看 GitHub 中的代码, 主要的区别是子类化了 UIPercentDrivenInteractiveTransition 并且重写了

- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;

如果你实在没有耐心了,可以直接使用我的 Cocoapods QIUPresentAnimation. 使用方法:

#import "QIUPresentKit.h"

UIViewController *nextController = [self.storyboard instantiateViewControllerWithIdentifier:@"EditViewController"];
QIUPresentViewController *controller = [[QIUPresentViewController alloc] initWithViewController:nextController];
[self presentViewController:controller animated:YES completion:nil];


上一篇:制作自己的 CocoaPods 下一篇:iOS 9 App Thinning