1、自定义转场
关于iOS 7出来的view controller转场api,网上有很多文章了,推荐去看这个iOS视图控制器转场详解,这里就不仔细介绍了,后面会讲一点用到的基本概念和内容。
2、我的AlertController
这是我的AlertController,相比系统自带的,多了几种动画,还有可以自定义字体,样式等等:
3、实现
1、看看系统的UIAlertController是怎么用的:
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"这是一个alert" message:@"又如何?" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
[alert addAction:cancel];
[self presentViewController:alert animated:YES completion:nil];
UIAlertController是继承UIViewController的,那么我们自定义一个KTAlertController同样继承UIViewController,然后presentViewController转场的时候展示KTAlertController就行了。我直接用的xib搭建KTAlertController,直观一点,也好利用自动布局。背景view设置为black alpha为0.3,因为展示UIViewController背景要透明黑。然后中间放contentView,设置圆角,各种子view,button等等。
这里考虑到title和description有可能没有或者多行的情况,也考虑到底部的button只有一个的情况,使用自动布局进行适配,整个alert view的大小会根据内容多少进行适配,具体的设置查看我的工程即可。
2、实际上你在调用[self presentViewController:alert animated:YES completion:nil]的时候,会发现系统已经给你做好了一个动画,是从底部往上弹,还有就是原来的view controller(也就是[self presentViewController:alert animated:YES completion:nil]中的self)不见了,我们要的不是这样的效果。这里presentViewController使用的是Modal转场,UIKit 已经为 Modal 转场实现了多种效果,当 UIViewController的modalPresentationStyle属性为UIModalPresentationCustom或UIModalPresentationFullScreen时,我们就有机会定制转场效果,此时modalTransitionStyle指定的转场动画将会被忽略。因此我们需要改写modalPresentationStyle的默认属性值,默认是UIModalTransitionStyleCoverVertical,也就是从下往上,原来的view controller不见了的形式。我们改为custom形式:
+ (instancetype)alertControllerWithTitle:(NSString *)title description:(NSString *)description cancel:(NSString *)cancel button:(NSString *)button action:(void (^)())buttonAction
{
NSAssert(title.length > 0 || description.length > 0 , @"title和description不能同时为空");
KTAlertController *alert = [[KTAlertController alloc] init];
alert.modalPresentationStyle = UIModalPresentationCustom;
alert.titleText = title;
alert.descriptionText = description;
alert.cancelText = cancel ? cancel : @"取消";
alert.buttonText = button;
alert.buttonAction = buttonAction;
return alert;
}
这样可以自定义效果,同时原来的view controller的view并不会从视图结构中删除。
3、转场动画控制器和转场代理
在自定义转场动画的时候需要提供给view controller转场代理,然后由转场代理在合适的时候提供给view controller转场动画控制器,由转场控制器告诉view controller你得怎么转场。比如拿我们熟悉的tableView来说,你会给tableView一个dataSource代理,然后tableView会调用代理方法tableView:cellForRowAtIndexPath:来问代理,我的每一行显示什么cell啊?这里是一样的,view controller也会问transitioningDelegate:
viewController:老兄,我马上要被present了,好激动啊,我该怎么出场闪瞎他们的狗眼呢?
transitioningDelegate:😓,好吧,我给你一个动画控制器吧,它里面有两个方法,会告诉你该怎么出场的。。。
viewController:哇,太好了,快给我吧!
这里transitioningDelegate就是用来提供转场动画控制器的,真正负责转场动画的,当然是转场动画控制器了。transitioningDelegate是普通的viewController都有的属性,定义是:
@interface UIViewController(UIViewControllerTransitioning)
@property (nullable, nonatomic, weak) id <UIViewControllerTransitioningDelegate> transitioningDelegate NS_AVAILABLE_IOS(7_0);
@end
这里遵守UIViewControllerTransitioningDelegate协议,对于UINavigationController还有遵守UINavigationControllerDelegate的属性delegate,对于UITabBarController还有遵守UITabBarControllerDelegate的属性delegate。
看看UIViewControllerTransitioningDelegate的定义:
@protocol UIViewControllerTransitioningDelegate <NSObject>
@optional
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);
@end
很显然第一个方法和第二个方法是分别在present和dismiss的时候提供动画控制器animationController的,这里的动画控制器必须遵守UIViewControllerAnimatedTransitioning协议,后面的三个方法是交互控制和返回presentationController的,暂时不用关心。
看来动画控制器不是随便一个object,必须遵守一定的规则才能让viewController知道如何动画,下面就是规则:
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@optional
// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;
@end
作为动画控制器,必须实现前面两个方法,第一个方法是告诉动画持续时间,第二个方法是告诉具体怎样动画。有了这两个,viewController就知道怎么动画了。
4、那么在构造器里面指定transitioningDelegate,这里指定自己为代理,modalPresentationStyle改为UIModalPresentationCustom
+ (instancetype)alertControllerWithTitle:(NSString *)title description:(NSString *)description cancel:(NSString *)cancel button:(NSString *)button action:(void (^)())buttonAction
{
NSAssert(title.length > 0 || description.length > 0 , @"title和description不能同时为空");
KTAlertController *alert = [[KTAlertController alloc] init];
alert.transitioningDelegate = alert;
alert.modalPresentationStyle = UIModalPresentationCustom;
alert.titleText = title;
alert.descriptionText = description;
alert.cancelText = cancel ? cancel : @"取消";
alert.buttonText = button;
alert.buttonAction = buttonAction;
return alert;
}
实现代理方法:
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
switch (self.animationType) {
case KTAlertControllerAnimationTypeCenterShow:
return [[KTCenterAnimationController alloc] init];
break;
case KTAlertControllerAnimationTypeUpDown:
return [[KTUpDownAnimationController alloc] init];
break;
default:
break;
}
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
switch (self.animationType) {
case KTAlertControllerAnimationTypeCenterShow:
return [[KTCenterAnimationController alloc] init];
break;
case KTAlertControllerAnimationTypeUpDown:
return [[KTUpDownAnimationController alloc] init];
break;
default:
break;
}
}
这里present和dismiss都用同一个动画控制器,当然可以分开写两个动画控制器分别控制present和dismiss,但是没必要哈,因为很简单。根据animationType(自定义的枚举属性)来返回不同的动画控制器实例,以此达到可以使用多种动画效果的目的。比如KTCenterAnimationController的实现:
// 头文件
#import <UIKit/UIKit.h>
@interface KTCenterAnimationController : NSObject <UIViewControllerAnimatedTransitioning>
@end
// 实现文件
#import "KTCenterAnimationController.h"
#import "KTAlertController.h"
@implementation KTCenterAnimationController
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 1
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
if (toVC.isBeingPresented) {
return 0.3;
}
else if (fromVC.isBeingDismissed) {
return 0.1;
}
return 0.3;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
KTAlertController *toVC = (KTAlertController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
if (!toVC || !fromVC) {
return;
}
UIView *containerView = [transitionContext containerView];
NSTimeInterval duration = [self transitionDuration:transitionContext];
if (toVC.isBeingPresented) {
// 2
[containerView addSubview:toVC.view];
toVC.view.frame = CGRectMake(0.0, 0.0, containerView.frame.size.width, containerView.frame.size.height);
toVC.backView.alpha = 0.0;
CGAffineTransform oldTransform = toVC.contentView.transform;
toVC.contentView.transform = CGAffineTransformScale(oldTransform, 0.3, 0.3);
toVC.contentView.center = containerView.center;
[UIView animateWithDuration:duration animations:^{
toVC.backView.alpha = 0.3;
toVC.contentView.transform = oldTransform;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
else if (fromVC.isBeingDismissed) {
// 3
[UIView animateWithDuration:duration animations:^{
fromVC.view.alpha = 0.0;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}
@end
1处返回动画时间根据present和dismiss区别,毕竟dismiss大家都喜欢它快一点滚是吧,这里在present和dismiss的时候,fromVC和toVC是不同的,在present的时候,我们的KTAlertController显然是toVC,但是dismiss的时候,它就变成了fromVC了,因为你是要从这个KTAlertController切换到底下的那个viewController。transitionContext这个参数可以告诉我们很多信息,fromVC, toVC,containerView等, containerView是装载要被present的KTAlertController的,这是系统提供的。进入视图调试,当一个KTAlertController被present之后,你可以看到一个透明的view,处于底下那个viewController的view和KTAlertController的view之间,就应该是containerView(我的理解哈)。
2处在present的时候,注意要将toVC的view添加到containerView,然后就是设置大小位置,动画等。
3处注意不需要添加fromVC的view到containerView上面,因为fromVC本来就是要消失的,toVC的view就更不能添加了,因为toVC此时就是底下的那个viewController。
4、后记
完整项目在这里,后续有时间再完善,还可以添加几种动画效果的!