原文:Transcending UIAlertView on iOS 7
作者: Matt Neuburg
译者:kmyhy
在我看来,在 iOS7 提供给开发者中的所有新特性中,最重要的莫过于一个 UI 方面的改进:自定义 view controller 转换,能够在 view controller 呈现时插入自己的动画。这样:
- 当 tab bar controller 的某个子控制器被选中时,你可以自定义动画。
- 当 view controller 以 push 方式展现到导航控制器中时,你不再受限于系统的“从右边滑入”的动画。
- 当一个 view controller 以弹出方式呈现/解散时,不再受限于 4 种 UIModalTransitionStyle 所指定的动画。
对于第三种情况——一个被呈现的 view controller —— iOS 7 引入了一个创举:你可以将被呈现控制器放在任何地方,甚至可以部分遮住原视图上面。
换句话说,被呈现控制器的 view 可以“浮”在或部分覆盖住原视图上方,用户可以同时看到两个视图,一个在前,一个在后。
这对于 iPhone 来说意义重大。以前,在 iPhone 上,被呈现控制器只能全屏覆盖——实际上取代了原视图。在 iOS 7,情况发生了变化,被呈现控制器的视图可以只占据部分屏幕,同时原来的 view controller 的视图也不会被移除。
为了展示这一点,我会介绍如何利用这个特性来代替原来单调的 UIAlertView,利用自定义的小巧的“漂浮式”的 view,可以以 UIAlertView 相同的方式来显示和解散。和 UIAlertView 不同,这个 view 可以任意定制你自己的 UI。
注意:本文摘自作者写的一本叫做《Programming iOS 7》的书,我为本文专门编写了一个 demo 项目:https://github.com/mattneub/custom-alert-view-iOS7
下面的截图显示了我们即要实现的效果:
左边是 app 的主界面,包含一个”Show Custom Alert View” 按钮(顶部)和一个 image view。目的仅仅是为我们的自定义的 alert view 提供一个足够醒目、漂浮于上的 UI。
右边是自定义 alert view 显示的样子,它漂浮在原来的界面上。注意,和 UIAlertView 一样,屏幕的剩余部分将变暗,并使”Show Custom Alert View” 按钮(在背景 view 上)变得模糊。但它显然不是一个普通的 UIAlertView:它包含了一个 image view 和一个 switch 控件!这是用常规的 UIAlertView 所无法做到的。当然,你可以在这个 view 上加入任何东西。我们突破了 UIAlertView 的限制。我们需要做的仅仅是定义一个 view controller 类,我把它命名为 CustomAlertViewController,以及一个同名的 .xib。这个 .xib 的大概样子如下图所示:
有两个地方值得注意:
- 外面的那个 view,它的大小占满了 iPhone 整个屏幕 ,它是控制器的根视图(即控制器的 view)。它的背景色是一个有点透明的淡灰色。这产生了一个效果,在 alert view 后面产生一个背景加暗的效果。
- 中间是稍小一些的 subview。它构成了我们的 alert view 的主要部分。CustomAlertViewController 中有一个 IBOutlet 引用了它,名字是 alertView。
这是 viewDidLoad 方法,它负责配置 self.alertView,给它一个蓝色的边框和圆角矩形的外观。
- (void)viewDidLoad {
[super viewDidLoad];
self.alertView.layer.borderColor = [UIColor blueColor].CGColor;
self.alertView.layer.borderWidth = 2;
self.alertView.layer.cornerRadius = 8;
}
CustomAlertViewController 有一个初始化方法:
-(id)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil
bundle:nibBundleOrNil];
if (self) {
self.modalPresentationStyle = UIModalPresentationCustom;
self.transitioningDelegate = self;
}
return self;
}
这里设置了两个 iOS 7 中新增的 UIViewController 属性。modalPresentationStyle 设置为 Custom,表示我们将为它提供自定义的转换动画。transitioningDelegate 设置为一个对象,由这个对象来负责提供自定义的转换动画。
然后回到主界面,处理 Show Custom Alert View 按钮的触摸。在这里创建 CustomAler letViewControl 实例并呈现它:
- (IBAction) doButton: (id) sender {
UIViewController* vc = [CustomAlertViewController new];
[self presentViewController:vc animated:YES completion:nil];
}
我们的 CustomAlertViewController 将被呈现,但是它 modalPresentationStyle 和 transitioningDelegate 还需要进一步处理。transitioningDelegate 被设为了 self, 也就是 CustomAlertViewController 自己,因此它需要采用 UIViewControllerTransitioningDelegate 协议。在 CustomAlertViewController 中需要实现两个方法,每个方法返回一个对象,由该对象来实际操作自定义转换动画。为了遵循对象的“自包含”原则,那个对象也被我们放到了 self 中:
```swift
-(id<UIViewControllerAnimatedTransitioning>)
animationControllerForPresentedController:
(UIViewController *)presented
presentingController:
(UIViewController *)presenting
sourceController:
(UIViewController *)source {
return self;
}
-(id<UIViewControllerAnimatedTransitioning>)
animationControllerForDismissedController:
(UIViewController *)dismissed {
return self;
}
<div class="se-preview-section-delimiter"></div>
两个方法都返回了 self,运行的时候需要 CustomAlertViewController 提供更多的细节来进行自定义转换动画(即实现 UIViewControllerAnimatedTransitioning 协议)。首先必须实现这个方法,以返回一个动画时长:
-(NSTimeInterval)transitionDuration:
(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
<div class="se-preview-section-delimiter"></div>
然后,真正执行自定义转换动画:
-(void)animateTransition:
(id<UIViewControllerContextTransitioning>)transitionContext {
// ...
}
<div class="se-preview-section-delimiter"></div>
我将分多次完成这个方法。首先看一下传入的参数,transitionContext。这个对象会在运行时提供给你,以便获取一些重要的信息比如:
- 和转换相关的两个 view controller。
- 主要是 container view 对象,在这个地方,两个 view controller 将被显示。可以将 container view 看做是动画发生的场所。
因此,我们的第一步就是从 transitionContext 中获取这几个信息:
UIViewController* vc1 =
[transitionContext viewControllerForKey:
UITransitionContextFromViewControllerKey];
UIViewController* vc2 =
[transitionContext viewControllerForKey:
UITransitionContextToViewControllerKey];
UIView* con = [transitionContext containerView];
UIView* v1 = vc1.view;
UIView* v2 = vc2.view;
<div class="se-preview-section-delimiter"></div>
现在我们就拿到了两个 view controller 和它们的 view 以及 containerView。
在 UIViewControllerAnimatedTransitioning 协议中,我们总共返回了两次 self,一次用于呈现,一次用于解散。我们必须区分这两种情况。其实很简单:当呈现时,第二个 view controller(vc2) 将是 self,它的 view (v2) 则是 self.view。
我会分别针对两种情况进行处理。当呈现的时候,在 container view 中已经有第一个 view controller 的 view(v1)存在了,我们的工作仅仅是将第二个 view controller 的 view(v2,也就是 self.view)添加到 container view(con)中去。把它放到我们想放的地方,并按照我们的方式对它进行动画。还有一个强制性的规定:当我们完成动画,我们必须发送一个消息(completeTransition:)给 transitionContext 对象,告诉它动画已经完成。
在我们的例子中,因为我们的 view 是一个背景上的“阴影 view”,它的 frame 应当等于位于它下面的原始 view 的 frame。为了造成一种 UIAlertView 飞进屏幕的效果,我们需要将我们的 view 从屏幕之外移动到屏幕之内。我们可以用一个缩放转换来完成,将我们的 alertView 从一个放大的尺寸变成正常大小,以模仿系统内置的 UIAlertView:
if (vc2 == self) { // 呈现过程
[con addSubview:v2];
v2.frame = v1.frame;
self.alertView.transform = CGAffineTransformMakeScale(1.6,1.6);
v2.alpha = 0;
v1.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
[UIView animateWithDuration:0.25 animations:^{
v2.alpha = 1;
self.alertView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
<div class="se-preview-section-delimiter"></div>
呈现就讲完了,现在来看解散。当用户点击 OK,我们应该解散我们的 alert view:
- (IBAction) doDismiss: (id) sender {
[self.presentingViewController
dismissViewControllerAnimated:YES completion:nil];
}
<div class="se-preview-section-delimiter"></div>
这同样会调用我们的 animateTransition: 方法。当我们进行解散过程时,view controller 会以不同方式进行:第二个 view controller(vc2)是原始视图,而第一个 view controller(vc1)是 self,他的 view(v1) 是我们的视图(self.view)。我们以动画的方式隐藏我们的 view,但不需要我们从 container view 中移除它,运行的时候会自动移除:
else { // 解散过程
[UIView animateWithDuration:0.25 animations:^{
self.alertView.transform = CGAffineTransformMakeScale(0.5,0.5);
v1.alpha = 0;
} completion:^(BOOL finished) {
v2.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
[transitionContext completeTransition:YES];
}];
}
注意在两个过程中我们修变了原始视图的 tintAdjustmentMode 属性,以和原生 UIAlertView 保持一致。
这就搞定了!有几个地方需要注意一下:
- 这个例子不管 iPhone 是横屏还是竖屏都能够正常工作。甚至你可以在 alert view 显示的时候旋转屏幕也不会有什么问题。因为我们的 view controller 同正常的 view controller 一样是支持旋屏的,无论原始 view 还是我们的 view(“阴影 view”,包括 alert view)都能以正常的方式旋转和改变大小。约束(xib 文件中的)保持让 alert view 始终处于“阴影 view”的中央。
- 还可以在 viewDidLoad 中将一个 UIMotionEffectdGroup 添加到 self.alertView 上。这样它会显示一种平行视差效果,和 UIAlertView 一样。
我希望这个例子能引导你创建自己的自定义 alert view。不需要再和 UIAlertView 较劲了,事实上,你从此不再需要 UIAlertView。你可以自由地创建自己的 view 并让它替代 UIAlertView。你可以随便修改这个例子,加入你自己的代码,让它更像或者更不像原生的 UIAlertView。没有什么事情是不可能的!