在話題 #5 中,Chris Eidhof 向我們介紹了 iOS7 引入的新特性自定義 View Controller 轉(zhuǎn)場(chǎng). 他給出了一個(gè) 結(jié)論:
我們?cè)诒疚闹惶接懥嗽?navigation controller 中的兩個(gè) view controller 之間的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),但是這些做法在 tab bar controller 或者任何你自己定義的 view controller 容器中也是通用的…
盡管從技術(shù)角度來(lái)講,使用 iOS 7 的 API,你可以對(duì)自定義容器中的 view controllers 做自定義轉(zhuǎn)場(chǎng),但是這不是能直接使用的,實(shí)現(xiàn)這種效果非常不容易。
請(qǐng)注意我正在討論的自定義視圖控制器容器 (custom container view controllers) 都是 UIViewController 的直接子類(lèi),而不是 UITabBarController 或者 UINavigationController 的子類(lèi)。
對(duì)于你自定義的繼承于 UIViewController 的容器子類(lèi),并沒(méi)有現(xiàn)成可用的 API 允許一個(gè)任意的動(dòng)畫(huà)控制器 (animation controller) 將一個(gè)子視圖控制器自動(dòng)轉(zhuǎn)場(chǎng)到另外一個(gè),不管是可交互式的轉(zhuǎn)場(chǎng)還是不可交互式的轉(zhuǎn)場(chǎng)。 我甚至都覺(jué)著蘋(píng)果根本就不想支持這種方式。蘋(píng)果支持下面的這幾種轉(zhuǎn)場(chǎng)方式:
在本文中,我將向你展示如何自定義視圖控制器容器,并且使其支持第三方的動(dòng)畫(huà)控制器。
如果你需要復(fù)習(xí)一下 iOS 5 引入的視圖控制器容器,請(qǐng)閱讀話題#1 中 Ricky Gregersen 寫(xiě)的文章 “View Controller 容器”。
看到這里,你可能對(duì)上文我們說(shuō)到的一些問(wèn)題犯嘀咕,讓我來(lái)告訴你答案吧:
為什么我們不直接繼承 UINavigationController 或 UITabBarController,并且使用它們提供的功能的?
有些時(shí)候這是你不想要的??赡苣阆胍粋€(gè)非常特殊的外觀或者行為,和這些類(lèi)能夠提供給你的差別非常大,因此你必須使用一些黑客式的手段去達(dá)到你想要的結(jié)果,同時(shí)還要擔(dān)心系統(tǒng)框架的版本更新后這些黑客式的手段是否還仍然有效?;蛘?,你就是想完全控制你的視圖控制器容器,避免不得不支持一些特定的功能。
好吧, 那么為什么不使用 transitionFromViewController:toViewController:duration:options:animations:completion: 去實(shí)現(xiàn)呢?
這又是一個(gè)好問(wèn)題,你可能想用這種方式去實(shí)現(xiàn),但是或許你對(duì)代碼的整潔性比較在意,想把這種轉(zhuǎn)場(chǎng)相關(guān)的代碼封裝在內(nèi)部。那么為什么不使用一個(gè)既存的、被良好驗(yàn)證的設(shè)計(jì)模式呢?這種設(shè)計(jì)模式可以非常方便的支持第三方的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。
在我們開(kāi)始寫(xiě)代碼之前,讓我們先花一分鐘的時(shí)間來(lái)簡(jiǎn)單看一下我們需要的組件吧。
iOS 7 自定義視圖控制器轉(zhuǎn)場(chǎng)的 API 基本上都是以協(xié)議的方式提供的,這也使其可以非常靈活的使用,因?yàn)槟憧梢院芎?jiǎn)單地將它們插入到你的類(lèi)中。最主要的五個(gè)組件如下:
UIViewControllerAnimatedTransitioning 協(xié)議,并且負(fù)責(zé)實(shí)際執(zhí)行動(dòng)畫(huà)。UIViewControllerInteractiveTransitioning 協(xié)議來(lái)控制可交互式的轉(zhuǎn)場(chǎng)。UIViewControllerContextTransitioning 協(xié)議,并且這是由系統(tǒng)負(fù)責(zé)生成和提供的。UIViewControllerTransitionCoordinator 協(xié)議。正如你從其他的閱讀材料中得知的那樣,轉(zhuǎn)場(chǎng)有不可交互式和可交互式兩種方式。在本文中,我們將集中精力于不可交互的轉(zhuǎn)場(chǎng)。這種轉(zhuǎn)場(chǎng)是最簡(jiǎn)單的轉(zhuǎn)場(chǎng),也是我們學(xué)習(xí)的一個(gè)好的開(kāi)始。這意味著我們需要處理上面提到的動(dòng)畫(huà)控制器 (animation controllers),轉(zhuǎn)場(chǎng)代理 (transitioning delegates) 和轉(zhuǎn)場(chǎng)上下文 (transitioning contexts)。
閑話少說(shuō),讓我們開(kāi)始動(dòng)手吧…
通過(guò)三個(gè)階段,我們將要實(shí)現(xiàn)一個(gè)簡(jiǎn)單自定義的視圖控制器容器,它可以對(duì)子視圖控制器提供自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的支持。
你可以在這里找到這三個(gè)階段的 Xcode 工程的源代碼。
我們應(yīng)用中的核心類(lèi)是 ContainerViewController,它持有一個(gè)UIViewController實(shí)例的數(shù)組,每個(gè)實(shí)例是一個(gè)普通的 ChildViewController。容器視圖控制器設(shè)置了一個(gè)帶有可點(diǎn)擊圖標(biāo),并代表每個(gè)子視圖控制器的私有的子視圖:
http://wiki.jikexueyuan.com/project/objc/images/12-20.gif" alt="" />
我們通過(guò)點(diǎn)擊圖標(biāo)在不同的子視圖控制器之間切換。在這一階段,子視圖控制器之間切換時(shí)是沒(méi)有轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的。
你可以在這里查看階段-1的源代碼。
當(dāng)我們添加轉(zhuǎn)場(chǎng)動(dòng)畫(huà)時(shí),我們想要使用一個(gè)遵從 UIViewControllerAnimatedTransitioning 協(xié)議的動(dòng)畫(huà)控制器(animation controllers)。這個(gè)協(xié)議聲明了 3 個(gè)方法,前面的 2 個(gè)方法是必須實(shí)現(xiàn)的:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animationEnded:(BOOL)transitionCompleted;
通過(guò)這些方法,我們可以獲得我們所需的所有東西。當(dāng)我們的視圖控制器容器準(zhǔn)備執(zhí)行動(dòng)畫(huà)時(shí),我們可以從動(dòng)畫(huà)控制器中獲取動(dòng)畫(huà)的持續(xù)時(shí)間,并讓其去執(zhí)行真正的動(dòng)畫(huà)。當(dāng)動(dòng)畫(huà)執(zhí)行完畢后,如果動(dòng)畫(huà)控制器實(shí)現(xiàn)了可選的 animationEnded: 方法,我們可以調(diào)用動(dòng)畫(huà)控制器中的 animationEnded: 方法。
但是,首先我們必須把一件事情搞清楚。正如你在上面的方法簽名中看到的那樣,上面兩個(gè)必須實(shí)現(xiàn)的方法需要一個(gè)轉(zhuǎn)場(chǎng)上下文參數(shù),這是一個(gè)遵從 UIViewControllerContextTransitioning 協(xié)議的對(duì)象。通常情況下,當(dāng)我們使用系統(tǒng)內(nèi)建的類(lèi)時(shí),系統(tǒng)框架為我們創(chuàng)建了轉(zhuǎn)場(chǎng)上下文對(duì)象,并把它傳遞給動(dòng)畫(huà)控制器。但是在我們這種情況下,我們需要自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà),所以我們需要承擔(dān)系統(tǒng)框架的責(zé)任,自己去創(chuàng)建這個(gè)轉(zhuǎn)場(chǎng)上下文對(duì)象。
這就是大量使用協(xié)議的方便之處。我們可以不用必須復(fù)寫(xiě)一個(gè)私有類(lèi),而復(fù)寫(xiě)私有類(lèi)這種方法是明顯不可行的。我們可以定義自己的類(lèi),并使其遵從文檔中相應(yīng)的協(xié)議就可以了。
盡管在 UIViewControllerContextTransitioning 協(xié)議中聲明了很多方法,而且它們都是必須要實(shí)現(xiàn) (required) 的,但是我們現(xiàn)在可以暫時(shí)忽略它們中的一些方法,因?yàn)槲覀儸F(xiàn)在僅僅支持不可交互式的轉(zhuǎn)場(chǎng)。
同 UIKit 類(lèi)似,我們定義了一個(gè)私有類(lèi) NSObject <UIViewControllerContextTransitioning>。在我們的特定例子中,這個(gè)私有類(lèi)是 PrivateTransitionContext,它的初始化方法如下實(shí)現(xiàn):
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight {
NSAssert ([fromViewController isViewLoaded] && fromViewController.view.superview, @"The fromViewController view must reside in the container view upon initializing the transition context.");
if ((self = [super init])) {
self.presentationStyle = UIModalPresentationCustom;
self.containerView = fromViewController.view.superview;
self.viewControllers = @{
UITransitionContextFromViewControllerKey:fromViewController,
UITransitionContextToViewControllerKey:toViewController,
};
CGFloat travelDistance = (goingRight ? -self.containerView.bounds.size.width : self.containerView.bounds.size.width);
self.disappearingFromRect = self.appearingToRect = self.containerView.bounds;
self.disappearingToRect = CGRectOffset (self.containerView.bounds, travelDistance, 0);
self.appearingFromRect = CGRectOffset (self.containerView.bounds, -travelDistance, 0);
}
return self;
}
我們把視圖的出現(xiàn)和消失時(shí)的狀態(tài)記錄了下來(lái),比如初始狀態(tài)和最終狀態(tài)的 frame。
請(qǐng)注意一點(diǎn),我們的初始化方法需要我們提供我們是在向右切換還是向左切換。在我們的 ContainerViewController 中,按鈕是一個(gè)接一個(gè)水平排列的,轉(zhuǎn)場(chǎng)上下文通過(guò)設(shè)置每個(gè)的 frame 來(lái)記錄它們之間的位置關(guān)系。動(dòng)畫(huà)控制器或者說(shuō) animator,在生成動(dòng)畫(huà)時(shí)可以使用這些 frame。
我們也可以通過(guò)另外的方式去獲取這些信息,但是那樣的話,就會(huì)使 animator 和 ContainerViewController 及其視圖控制器耦合在一起了,這是不好的,我們并不想這樣。animator 應(yīng)該只關(guān)心它自己以及傳遞給它的上下文,因?yàn)檫@樣,在理想情況下,animator 可以在不同的上下文中得到復(fù)用。
在下一步實(shí)現(xiàn)我們自己的動(dòng)畫(huà)控制器時(shí),我們應(yīng)該時(shí)刻記住這一點(diǎn),現(xiàn)在讓我們來(lái)實(shí)現(xiàn)轉(zhuǎn)場(chǎng)上下文吧。
你可能記得我們?cè)?issue #5 中的View Controller 轉(zhuǎn)場(chǎng)已經(jīng)做過(guò)相同的事情了,為什么我們不使用它呢?事實(shí)上,由于使用了非常靈活的協(xié)議,我們可以直接把那個(gè)工程中的動(dòng)畫(huà)控制器,也就是 Animator 類(lèi)直接拿過(guò)來(lái)使用,不需要任何修改。
使用 Animator 類(lèi)的實(shí)例來(lái)做轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的核心代碼如下所示:
[fromViewController willMoveToParentViewController:nil];
[self addChildViewController:toViewController];
Animator *animator = [[Animator alloc] init];
NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController];
NSUInteger toIndex = [self.viewControllers indexOfObject:toViewController];
PrivateTransitionContext *transitionContext = [[PrivateTransitionContext alloc] initWithFromViewController:fromViewController toViewController:toViewController goingRight:toIndex > fromIndex];
transitionContext.animated = YES;
transitionContext.interactive = NO;
transitionContext.completionBlock = ^(BOOL didComplete) {
[fromViewController.view removeFromSuperview];
[fromViewController removeFromParentViewController];
[toViewController didMoveToParentViewController:self];
};
[animator animateTransition:transitionContext];
這其中的大部分是在對(duì)視圖控制器容器的操作,計(jì)算出我們是在向左切換還是向右切換。做動(dòng)畫(huà)的部分基本上只有 3 行代碼:1) 創(chuàng)建 animator,2) 創(chuàng)建轉(zhuǎn)場(chǎng)上下文,和 3) 觸發(fā)動(dòng)畫(huà)執(zhí)行。
有了上面的代碼,轉(zhuǎn)場(chǎng)效果看起來(lái)如下圖所示:
http://wiki.jikexueyuan.com/project/objc/images/12-21.gif" alt="" />
非常酷,我們甚至沒(méi)有寫(xiě)一行動(dòng)畫(huà)相關(guān)的代碼。
你可以在 階段-2 標(biāo)簽下看到這部分代碼的變化。在與 階段-1 的對(duì)比這里你可以看到 階段-2 和 階段-1 相對(duì)比的完整的代碼改變。
我想我們最后要做的一件事情是封裝 ContainerViewController ,使其能夠:
這意味著我們需要把對(duì) Animator 類(lèi)的依賴移除,同時(shí)需要?jiǎng)?chuàng)建一個(gè)代理協(xié)議。
我們?nèi)缦露x這個(gè)協(xié)議:
@protocol ContainerViewControllerDelegate <NSObject>
@optional
- (void)containerViewController:(ContainerViewController *)containerViewController didSelectViewController:(UIViewController *)viewController;
- (id <UIViewControllerAnimatedTransitioning>)containerViewController:(ContainerViewController *)containerViewController animationControllerForTransitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController;
@end
containerViewController:didSelectViewController: 方法使 ContainerViewController 可以很更容易的集成于功能齊全的應(yīng)用中。
containerViewController:animationControllerForTransitionFromViewController:toViewController: 方法挺有趣的,當(dāng)然,你可以把它和下面的 UIKit 中的視圖控制器容器的代理協(xié)議做對(duì)比:
tabBarController:animationControllerForTransitionFromViewController:toViewController: (UITabBarControllerDelegate)navigationController:animationControllerForOperation:fromViewController:toViewController: (UINavigationControllerDelegate)所有的這些方法都返回一個(gè) id<UIViewControllerAnimatedTransitioning> 對(duì)象。
與之前一直使用一個(gè) Animator 對(duì)象不同, 我們現(xiàn)在可以從我們的代理那里獲取一個(gè)動(dòng)畫(huà)控制器:
id<UIViewControllerAnimatedTransitioning>animator = nil;
if ([self.delegate respondsToSelector:@selector (containerViewController:animationControllerForTransitionFromViewController:toViewController:)]) {
animator = [self.delegate containerViewController:self animationControllerForTransitionFromViewController:fromViewController toViewController:toViewController];
}
animator = (animator ?: [[PrivateAnimatedTransition alloc] init]);
如果我們有代理并且它返回了一個(gè) animator,那么我們就使用這個(gè) animator。否則,我們使用內(nèi)部私有類(lèi) PrivateAnimatedTransition 創(chuàng)建一個(gè)默認(rèn)的 animator。接下來(lái)我們將實(shí)現(xiàn) PrivateAnimatedTransition 類(lèi)。
盡管默認(rèn)的動(dòng)畫(huà)和 Animator 有一些不同,但是代碼看起來(lái)驚人的相似。下面是完整的代碼實(shí)現(xiàn):
@implementation PrivateAnimatedTransition
static CGFloat const kChildViewPadding = 16;
static CGFloat const kDamping = 0.75f;
static CGFloat const kInitialSpringVelocity = 0.5f;
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 1;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// When sliding the views horizontally, in and out, figure out whether we are going left or right.
BOOL goingRight = ([transitionContext initialFrameForViewController:toViewController].origin.x < [transitionContext finalFrameForViewController:toViewController].origin.x);
CGFloat travelDistance = [transitionContext containerView].bounds.size.width + kChildViewPadding;
CGAffineTransform travel = CGAffineTransformMakeTranslation (goingRight ? travelDistance : -travelDistance, 0);
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
toViewController.view.transform = CGAffineTransformInvert (travel);
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:kDamping initialSpringVelocity:kInitialSpringVelocity options:0x00 animations:^{
fromViewController.view.transform = travel;
fromViewController.view.alpha = 0;
toViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.alpha = 1;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end
需要注意的一點(diǎn)是,上面的代碼沒(méi)有通過(guò)設(shè)置視圖的 frame 來(lái)反應(yīng)它們之間的位置關(guān)系,但是代碼仍然可以正常工作,只不過(guò)轉(zhuǎn)場(chǎng)總是在同一個(gè)方向上。因此,這個(gè)類(lèi)也可以被其他的代碼庫(kù)使用。
轉(zhuǎn)場(chǎng)動(dòng)畫(huà)現(xiàn)在看起來(lái)如下所示:
http://wiki.jikexueyuan.com/project/objc/images/12-22.gif" alt="" />
在 階段-3 的代碼中,app delegate 中設(shè)置代理的部分被注釋掉了,這樣就可以看到默認(rèn)的動(dòng)畫(huà)效果了。你可以將其設(shè)置回再使用 Animator 類(lèi)。你可能想查看同 階段-2 相比所有的修改。
我們現(xiàn)在有一個(gè)自包含的提供了默認(rèn)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的 ContainerViewController 類(lèi),這個(gè)默認(rèn)的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)可以被開(kāi)發(fā)者自己定義的iOS 7 自定義動(dòng)畫(huà)控制器 (UIViewControllerAnimatedTransitioning) 的對(duì)象代替,甚至都可以不用關(guān)心我們的源代碼就可以方便的替換。
在本文中我們通過(guò)使用 iOS 7 提供的自定義視圖控制器轉(zhuǎn)場(chǎng)的新特性,使我們自定義的視圖控制器容器成為了 UIKit 的一等公民。
這意味著你可以把自定義的非交互式的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)應(yīng)用到自定義的視圖控制器容器中。你可以看到我們把 7 個(gè)話題之前使用的轉(zhuǎn)場(chǎng)類(lèi)直接拿過(guò)來(lái)使用,而且沒(méi)有做任何修改。
譯者注 即 issue #5 中的 View Controller 轉(zhuǎn)場(chǎng)中的
Animator類(lèi)。
如果你想讓自己的容器視圖控制器作為一個(gè)類(lèi)庫(kù)或者框架,或者僅僅想使你的代碼得到更好的復(fù)用,這將是非常完美的。
我們現(xiàn)在僅僅支持非交互式的轉(zhuǎn)場(chǎng),下一步就是對(duì)交互式的轉(zhuǎn)場(chǎng)也提供支持。
我把它留給你當(dāng)作一個(gè)練習(xí)。這有一些復(fù)雜,因?yàn)槲覀兓旧鲜且7孪到y(tǒng)的行為,而這真的全是猜測(cè)性的工作。