鍍金池/ 教程/ iOS/ 自定義 ViewController 容器轉(zhuǎn)場
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

自定義 ViewController 容器轉(zhuǎn)場

話題 #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)場方式:

  • Navigation controller 推入和推出頁面
  • Tab bar controller 選擇的改變
  • Modal 頁面的展示和消失

在本文中,我將向你展示如何自定義視圖控制器容器,并且使其支持第三方的動畫控制器。

如果你需要復(fù)習(xí)一下 iOS 5 引入的視圖控制器容器,請閱讀話題#1Ricky Gregersen 寫的文章 “View Controller 容器”。

預(yù)熱準(zhǔn)備

看到這里,你可能對上文我們說到的一些問題犯嘀咕,讓我來告訴你答案吧:

為什么我們不直接繼承 UINavigationControllerUITabBarController,并且使用它們提供的功能的?

有些時候這是你不想要的。可能你想要一個非常特殊的外觀或者行為,和這些類能夠提供給你的差別非常大,因此你必須使用一些黑客式的手段去達到你想要的結(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)場動畫。

介紹相關(guān)的API

在我們開始寫代碼之前,讓我們先花一分鐘的時間來簡單看一下我們需要的組件吧。

iOS 7 自定義視圖控制器轉(zhuǎn)場的 API 基本上都是以協(xié)議的方式提供的,這也使其可以非常靈活的使用,因為你可以很簡單地將它們插入到你的類中。最主要的五個組件如下:

  1. 動畫控制器 (Animation Controllers) 遵從 UIViewControllerAnimatedTransitioning 協(xié)議,并且負責(zé)實際執(zhí)行動畫。
  2. 交互控制器 (Interaction Controllers) 通過遵從 UIViewControllerInteractiveTransitioning 協(xié)議來控制可交互式的轉(zhuǎn)場。
  3. 轉(zhuǎn)場代理 (Transitioning Delegates) 根據(jù)不同的轉(zhuǎn)場類型方便的提供需要的動畫控制器和交互控制器。
  4. 轉(zhuǎn)場上下文 (Transitioning Contexts) 定義了轉(zhuǎn)場時需要的元數(shù)據(jù),比如在轉(zhuǎn)場過程中所參與的視圖控制器和視圖的相關(guān)屬性。 轉(zhuǎn)場上下文對象遵從 UIViewControllerContextTransitioning 協(xié)議,并且這是由系統(tǒng)負責(zé)生成和提供的
  5. 轉(zhuǎn)場協(xié)調(diào)器(Transition Coordinators) 可以在運行轉(zhuǎn)場動畫時,并行的運行其他動畫。 轉(zhuǎn)場協(xié)調(diào)器遵從 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 工程的源代碼。

階段 1: 基礎(chǔ)

我們應(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的源代碼。

階段 2: 轉(zhuǎn)場動畫

當(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 相對比的完整的代碼改變。

階段 3: 封裝

我想我們最后要做的一件事情是封裝 ContainerViewController ,使其能夠:

  1. 提供默認(rèn)的轉(zhuǎn)場動畫。
  2. 提供替換默認(rèn)動畫控制器的代理。

這意味著我們需要把對 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)心我們的源代碼就可以方便的替換。

結(jié)論

在本文中我們通過使用 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)的行為,而這真的全是猜測性的工作。

擴展資料