在話題 #5 中,Chris Eidhof 向我們介紹了 iOS7 引入的新特性自定義 View Controller 轉(zhuǎn)場. 他給出了一個 結(jié)論:
我們在本文只探討了在 navigation controller 中的兩個 view controller 之間的轉(zhuǎn)場動畫,但是這些做法在 tab bar controller 或者任何你自己定義的 view controller 容器中也是通用的…
盡管從技術(shù)角度來講,使用 iOS 7 的 API,你可以對自定義容器中的 view controllers 做自定義轉(zhuǎn)場,但是這不是能直接使用的,實現(xiàn)這種效果非常不容易。
請注意我正在討論的自定義視圖控制器容器 (custom container view controllers) 都是 UIViewController
的直接子類,而不是 UITabBarController
或者 UINavigationController
的子類。
對于你自定義的繼承于 UIViewController
的容器子類,并沒有現(xiàn)成可用的 API 允許一個任意的動畫控制器 (animation controller) 將一個子視圖控制器自動轉(zhuǎn)場到另外一個,不管是可交互式的轉(zhuǎn)場還是不可交互式的轉(zhuǎn)場。 我甚至都覺著蘋果根本就不想支持這種方式。蘋果支持下面的這幾種轉(zhuǎn)場方式:
在本文中,我將向你展示如何自定義視圖控制器容器,并且使其支持第三方的動畫控制器。
如果你需要復(fù)習(xí)一下 iOS 5 引入的視圖控制器容器,請閱讀話題#1 中 Ricky Gregersen 寫的文章 “View Controller 容器”。
看到這里,你可能對上文我們說到的一些問題犯嘀咕,讓我來告訴你答案吧:
為什么我們不直接繼承 UINavigationController
或 UITabBarController
,并且使用它們提供的功能的?
有些時候這是你不想要的。可能你想要一個非常特殊的外觀或者行為,和這些類能夠提供給你的差別非常大,因此你必須使用一些黑客式的手段去達到你想要的結(jié)果,同時還要擔(dān)心系統(tǒng)框架的版本更新后這些黑客式的手段是否還仍然有效。或者,你就是想完全控制你的視圖控制器容器,避免不得不支持一些特定的功能。
好吧, 那么為什么不使用 transitionFromViewController:toViewController:duration:options:animations:completion:
去實現(xiàn)呢?
這又是一個好問題,你可能想用這種方式去實現(xiàn),但是或許你對代碼的整潔性比較在意,想把這種轉(zhuǎn)場相關(guān)的代碼封裝在內(nèi)部。那么為什么不使用一個既存的、被良好驗證的設(shè)計模式呢?這種設(shè)計模式可以非常方便的支持第三方的轉(zhuǎn)場動畫。
在我們開始寫代碼之前,讓我們先花一分鐘的時間來簡單看一下我們需要的組件吧。
iOS 7 自定義視圖控制器轉(zhuǎn)場的 API 基本上都是以協(xié)議的方式提供的,這也使其可以非常靈活的使用,因為你可以很簡單地將它們插入到你的類中。最主要的五個組件如下:
UIViewControllerAnimatedTransitioning
協(xié)議,并且負責(zé)實際執(zhí)行動畫。UIViewControllerInteractiveTransitioning
協(xié)議來控制可交互式的轉(zhuǎn)場。UIViewControllerContextTransitioning
協(xié)議,并且這是由系統(tǒng)負責(zé)生成和提供的。UIViewControllerTransitionCoordinator
協(xié)議。正如你從其他的閱讀材料中得知的那樣,轉(zhuǎn)場有不可交互式和可交互式兩種方式。在本文中,我們將集中精力于不可交互的轉(zhuǎn)場。這種轉(zhuǎn)場是最簡單的轉(zhuǎn)場,也是我們學(xué)習(xí)的一個好的開始。這意味著我們需要處理上面提到的動畫控制器 (animation controllers),轉(zhuǎn)場代理 (transitioning delegates) 和轉(zhuǎn)場上下文 (transitioning contexts)。
閑話少說,讓我們開始動手吧…
通過三個階段,我們將要實現(xiàn)一個簡單自定義的視圖控制器容器,它可以對子視圖控制器提供自定義的轉(zhuǎn)場動畫的支持。
你可以在這里找到這三個階段的 Xcode 工程的源代碼。
我們應(yīng)用中的核心類是 ContainerViewController
,它持有一個UIViewController
實例的數(shù)組,每個實例是一個普通的 ChildViewController
。容器視圖控制器設(shè)置了一個帶有可點擊圖標(biāo),并代表每個子視圖控制器的私有的子視圖:
http://wiki.jikexueyuan.com/project/objc/images/12-20.gif" alt="" />
我們通過點擊圖標(biāo)在不同的子視圖控制器之間切換。在這一階段,子視圖控制器之間切換時是沒有轉(zhuǎn)場動畫的。
你可以在這里查看階段-1的源代碼。
當(dāng)我們添加轉(zhuǎn)場動畫時,我們想要使用一個遵從 UIViewControllerAnimatedTransitioning
協(xié)議的動畫控制器(animation controllers)。這個協(xié)議聲明了 3 個方法,前面的 2 個方法是必須實現(xiàn)的:
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
- (void)animationEnded:(BOOL)transitionCompleted;
通過這些方法,我們可以獲得我們所需的所有東西。當(dāng)我們的視圖控制器容器準(zhǔn)備執(zhí)行動畫時,我們可以從動畫控制器中獲取動畫的持續(xù)時間,并讓其去執(zhí)行真正的動畫。當(dāng)動畫執(zhí)行完畢后,如果動畫控制器實現(xiàn)了可選的 animationEnded:
方法,我們可以調(diào)用動畫控制器中的 animationEnded:
方法。
但是,首先我們必須把一件事情搞清楚。正如你在上面的方法簽名中看到的那樣,上面兩個必須實現(xiàn)的方法需要一個轉(zhuǎn)場上下文參數(shù),這是一個遵從 UIViewControllerContextTransitioning
協(xié)議的對象。通常情況下,當(dāng)我們使用系統(tǒng)內(nèi)建的類時,系統(tǒng)框架為我們創(chuàng)建了轉(zhuǎn)場上下文對象,并把它傳遞給動畫控制器。但是在我們這種情況下,我們需要自定義轉(zhuǎn)場動畫,所以我們需要承擔(dān)系統(tǒng)框架的責(zé)任,自己去創(chuàng)建這個轉(zhuǎn)場上下文對象。
這就是大量使用協(xié)議的方便之處。我們可以不用必須復(fù)寫一個私有類,而復(fù)寫私有類這種方法是明顯不可行的。我們可以定義自己的類,并使其遵從文檔中相應(yīng)的協(xié)議就可以了。
盡管在 UIViewControllerContextTransitioning
協(xié)議中聲明了很多方法,而且它們都是必須要實現(xiàn) (required) 的,但是我們現(xiàn)在可以暫時忽略它們中的一些方法,因為我們現(xiàn)在僅僅支持不可交互式的轉(zhuǎn)場。
同 UIKit 類似,我們定義了一個私有類 NSObject <UIViewControllerContextTransitioning>
。在我們的特定例子中,這個私有類是 PrivateTransitionContext
,它的初始化方法如下實現(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)和消失時的狀態(tài)記錄了下來,比如初始狀態(tài)和最終狀態(tài)的 frame。
請注意一點,我們的初始化方法需要我們提供我們是在向右切換還是向左切換。在我們的 ContainerViewController
中,按鈕是一個接一個水平排列的,轉(zhuǎn)場上下文通過設(shè)置每個的 frame 來記錄它們之間的位置關(guān)系。動畫控制器或者說 animator,在生成動畫時可以使用這些 frame。
我們也可以通過另外的方式去獲取這些信息,但是那樣的話,就會使 animator 和 ContainerViewController
及其視圖控制器耦合在一起了,這是不好的,我們并不想這樣。animator 應(yīng)該只關(guān)心它自己以及傳遞給它的上下文,因為這樣,在理想情況下,animator 可以在不同的上下文中得到復(fù)用。
在下一步實現(xiàn)我們自己的動畫控制器時,我們應(yīng)該時刻記住這一點,現(xiàn)在讓我們來實現(xiàn)轉(zhuǎn)場上下文吧。
你可能記得我們在 issue #5 中的View Controller 轉(zhuǎn)場已經(jīng)做過相同的事情了,為什么我們不使用它呢?事實上,由于使用了非常靈活的協(xié)議,我們可以直接把那個工程中的動畫控制器,也就是 Animator
類直接拿過來使用,不需要任何修改。
使用 Animator
類的實例來做轉(zhuǎn)場動畫的核心代碼如下所示:
[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];
這其中的大部分是在對視圖控制器容器的操作,計算出我們是在向左切換還是向右切換。做動畫的部分基本上只有 3 行代碼:1) 創(chuàng)建 animator,2) 創(chuàng)建轉(zhuǎn)場上下文,和 3) 觸發(fā)動畫執(zhí)行。
有了上面的代碼,轉(zhuǎn)場效果看起來如下圖所示:
http://wiki.jikexueyuan.com/project/objc/images/12-21.gif" alt="" />
非???,我們甚至沒有寫一行動畫相關(guān)的代碼。
你可以在 階段-2 標(biāo)簽下看到這部分代碼的變化。在與 階段-1 的對比這里你可以看到 階段-2 和 階段-1 相對比的完整的代碼改變。
我想我們最后要做的一件事情是封裝 ContainerViewController
,使其能夠:
這意味著我們需要把對 Animator
類的依賴移除,同時需要創(chuàng)建一個代理協(xié)議。
我們?nèi)缦露x這個協(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é)議做對比:
tabBarController:animationControllerForTransitionFromViewController:toViewController:
(UITabBarControllerDelegate
)navigationController:animationControllerForOperation:fromViewController:toViewController:
(UINavigationControllerDelegate
)所有的這些方法都返回一個 id<UIViewControllerAnimatedTransitioning>
對象。
與之前一直使用一個 Animator
對象不同, 我們現(xiàn)在可以從我們的代理那里獲取一個動畫控制器:
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]);
如果我們有代理并且它返回了一個 animator,那么我們就使用這個 animator。否則,我們使用內(nèi)部私有類 PrivateAnimatedTransition
創(chuàng)建一個默認(rèn)的 animator。接下來我們將實現(xiàn) PrivateAnimatedTransition
類。
盡管默認(rèn)的動畫和 Animator
有一些不同,但是代碼看起來驚人的相似。下面是完整的代碼實現(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
需要注意的一點是,上面的代碼沒有通過設(shè)置視圖的 frame 來反應(yīng)它們之間的位置關(guān)系,但是代碼仍然可以正常工作,只不過轉(zhuǎn)場總是在同一個方向上。因此,這個類也可以被其他的代碼庫使用。
轉(zhuǎn)場動畫現(xiàn)在看起來如下所示:
http://wiki.jikexueyuan.com/project/objc/images/12-22.gif" alt="" />
在 階段-3 的代碼中,app delegate 中設(shè)置代理的部分被注釋掉了,這樣就可以看到默認(rèn)的動畫效果了。你可以將其設(shè)置回再使用 Animator
類。你可能想查看同 階段-2 相比所有的修改。
我們現(xiàn)在有一個自包含的提供了默認(rèn)轉(zhuǎn)場動畫的 ContainerViewController
類,這個默認(rèn)的轉(zhuǎn)場動畫可以被開發(fā)者自己定義的iOS 7 自定義動畫控制器 (UIViewControllerAnimatedTransitioning
) 的對象代替,甚至都可以不用關(guān)心我們的源代碼就可以方便的替換。
在本文中我們通過使用 iOS 7 提供的自定義視圖控制器轉(zhuǎn)場的新特性,使我們自定義的視圖控制器容器成為了 UIKit 的一等公民。
這意味著你可以把自定義的非交互式的轉(zhuǎn)場動畫應(yīng)用到自定義的視圖控制器容器中。你可以看到我們把 7 個話題之前使用的轉(zhuǎn)場類直接拿過來使用,而且沒有做任何修改。
譯者注 即 issue #5 中的 View Controller 轉(zhuǎn)場中的
Animator
類。
如果你想讓自己的容器視圖控制器作為一個類庫或者框架,或者僅僅想使你的代碼得到更好的復(fù)用,這將是非常完美的。
我們現(xiàn)在僅僅支持非交互式的轉(zhuǎn)場,下一步就是對交互式的轉(zhuǎn)場也提供支持。
我把它留給你當(dāng)作一個練習(xí)。這有一些復(fù)雜,因為我們基本上是要模仿系統(tǒng)的行為,而這真的全是猜測性的工作。