————譯自iOS Animation Tutorial: Custom View Controller Presentation Transitions,英文鏈接:https://www.raywenderlich.com/146692/ios-animation-tutorial-custom-view-controller-presentation-transitions-2
工程教學(xué)代碼:https://koenig-media.raywenderlich.com/uploads/2016/10/Custom_View_Controller_starter.zip
工程完整代碼:https://koenig-media.raywenderlich.com/uploads/2016/10/Custom_View_Controller_final.zip
背后的機(jī)制
UIKIts框架允許我們通過(guò)代理來(lái)自定義controllers之間的present動(dòng)畫,可以通過(guò)遵守UIViewControllerTransitioningDelegate來(lái)實(shí)現(xiàn)。
每次我們present一個(gè)新的controller的時(shí)候,UIKits會(huì)詢問(wèn)他的代理是否應(yīng)該使用自定義的轉(zhuǎn)場(chǎng)動(dòng)畫,這里是自定義轉(zhuǎn)場(chǎng)動(dòng)畫實(shí)現(xiàn)的第一步,如下圖:

UIKit調(diào)用animationController(forPresented:presenting:source:)代理方法來(lái)查看是否返回了一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象,
如果這個(gè)方法返回的是空,UIKit就會(huì)使用默認(rèn)的present動(dòng)畫,如果UIKit接收到了一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象,那么UIKit就會(huì)使用那個(gè)對(duì)象作為present動(dòng)畫的控制器。
下面是UIKit使用自定義動(dòng)畫控制器前的幾步其他操作:

UIKit首次詢問(wèn)他的動(dòng)畫控制器來(lái)獲得動(dòng)畫的持續(xù)時(shí)間(以秒為單位)然后調(diào)用animateTransition(using:)方法,這是我們自定義動(dòng)畫開(kāi)始真正發(fā)生的地方。
在animateTransition(using:)方法中,我們既可以訪問(wèn)到當(dāng)前正展示在screen上的viewController,同時(shí)也可以訪問(wèn)到即將被present出來(lái)的viewController,你可以按照自己想要的對(duì)當(dāng)前view和即將出現(xiàn)的新view進(jìn)行漸隱漸現(xiàn)、縮放、旋轉(zhuǎn)等等操作。
現(xiàn)在已經(jīng)學(xué)習(xí)到了一點(diǎn)自定義present動(dòng)畫是如何工作的,那么就可以開(kāi)始自己的了。
實(shí)現(xiàn)transitioning動(dòng)畫代理
既然代理的任務(wù)是控制產(chǎn)生真正動(dòng)畫的動(dòng)畫發(fā)生器,那么在寫代理里面的方法前應(yīng)該先為這個(gè)動(dòng)畫類創(chuàng)建一個(gè)存根(存根的意思是????、)。在項(xiàng)目里新建一個(gè)類,繼承與NSObject,可以取名為PopAnimator,原文中使用swift語(yǔ)言寫的,這里用OC,然后打開(kāi)新建的類,讓它遵守UIViewControllerAnimatedTransitioning協(xié)議、可以看到xcode給出警告,因?yàn)槲覀冞€沒(méi)有實(shí)現(xiàn)它required的代理方法。
然后打開(kāi).m文件,向其中添加如下方法,
```
OC:- (NSTimeInterval)transitionDuration:(nullable id )transitionContext{
? ? return:0;
}
Swift:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
?return 0
}
```
以上返回的0值只是起到一個(gè)占位的作用,可以在之后用真正的值來(lái)代替它。
現(xiàn)在向PopAnimator里繼續(xù)添加方法:
```
oc:
-(void)animateTransition:(id )transitionContext{
}
swift:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
以上的方法將會(huì)持有我們的動(dòng)畫代碼,加上這個(gè)方法后就可以去掉Xcode提示的警告了。
現(xiàn)在既然已經(jīng)已經(jīng)擁有了基礎(chǔ)的動(dòng)畫類,那么可以轉(zhuǎn)移到viewController這邊來(lái)實(shí)現(xiàn)代理方法了。
打開(kāi)viewcontroller.h(或者viewcontroller.swift),然后然他遵守UIViewControllerTransitioningDelegate
這意味著我們的viewController遵守了專場(chǎng)協(xié)議。你可以將它的代理的方法添加到你的.m文件里
找到disTapImageView:(_:)這個(gè)方法,在方法的底部可以看到present出詳情頁(yè)的代碼,herbDetails是一個(gè)新的viewCotroller實(shí)例,你可以將他的專場(chǎng)代理設(shè)置為當(dāng)前的controller。
herbDetails.transitioningDelegate = self;
現(xiàn)在你每次present出這個(gè)新的詳情頁(yè)的時(shí)候,UIKit都會(huì)詢問(wèn)當(dāng)前viewController是否擁有一個(gè)動(dòng)畫控制器。然而到現(xiàn)在我們viewController里還沒(méi)有實(shí)現(xiàn)任何關(guān)于UIViewControllerTransitioningDelegate代理的方法。因此UIKit依然會(huì)使用系統(tǒng)默認(rèn)的動(dòng)畫。
下一步就是創(chuàng)建我們的動(dòng)畫控制器了,然后在UIKit每次詢問(wèn)的時(shí)候返回給它用來(lái)執(zhí)行動(dòng)畫。
OC:PopAnimator *transition = [[PopAnimator alloc]init];
Swift: let ?transition = PopAnimator();
(不得不說(shuō),Swift確實(shí)夠簡(jiǎn)潔,正像它的名字的含義一樣)
這個(gè)PopAnimator的實(shí)例transition將會(huì)執(zhí)行我們的轉(zhuǎn)場(chǎng)動(dòng)畫了,我們只需要?jiǎng)?chuàng)建一個(gè)這個(gè)對(duì)象,因?yàn)槲覀兠看蔚膭?dòng)畫都是一樣的,所以每次present一個(gè)controller的時(shí)候都可以用這個(gè)實(shí)例。
現(xiàn)在打開(kāi)我們的ViewController,添加以下的代理方法。
OC:
- (nullableid )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
//返回一個(gè)遵守了UIViewControllerAnimatedTransitioning協(xié)議的對(duì)象。
}
Swift:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
?return transition
}
在這個(gè)方法里攜帶了幾個(gè)參數(shù)以便于讓你決定要不要返回一個(gè)自定義的動(dòng)畫。在這里我們只需要返回一個(gè)PopAnimator的實(shí)例即可。
現(xiàn)在我們已經(jīng)為present出controller添加了代理的方法,但是我們?cè)撛鯓觗ismiss掉呢
可以繼續(xù)添加代理方法如下:
OC: - (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed;
Swift:
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
?return nil
}
上面的方法本質(zhì)上做了和上一個(gè)同樣的事情:你檢查哪一個(gè)viewController被dismiss掉了,然后決定是否應(yīng)該返回nil繼續(xù)使用系統(tǒng)默認(rèn)的動(dòng)畫,或者是返回一個(gè)自定義的動(dòng)畫來(lái)代替。在這里我們返回nil即可。
現(xiàn)在我們已經(jīng)擁有了一個(gè)自定義的動(dòng)畫控制器來(lái)控制我們的自定義的轉(zhuǎn)場(chǎng)動(dòng)畫。但是它怎樣工作呢?如果這時(shí)候運(yùn)行工程然后run,然后會(huì)發(fā)現(xiàn)沒(méi)有發(fā)生任何事情,為什么?因?yàn)槲覀兊淖远x動(dòng)畫控制器里還沒(méi)有任何代碼呢,哈哈!
那么現(xiàn)在我們寫PopAnimator里的代碼
首先給類設(shè)置幾個(gè)成員變量如下
OC:
{
? ? CGFloat ? duration;(默認(rèn)為1)
? ? BOOL ?presenting;(默認(rèn)為true)
? ? CGRect originFrame;(默認(rèn)為CGRectZero)
}
Swift:
let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
此處我們的duration可以用在幾個(gè)不同的地方,比如當(dāng)你想告訴UIKit我們的動(dòng)畫將會(huì)持續(xù)多久還有當(dāng)你創(chuàng)建自己的動(dòng)畫時(shí)也會(huì)用到。
我們也定義了presenting 布爾值,來(lái)告訴動(dòng)畫控制器我們是在present還是在dismiss。我們之所以想記錄這個(gè)值,因?yàn)槲覀冋梢杂眠@個(gè)來(lái)present,反過(guò)來(lái)就可以用來(lái)dismiss了。
最后我們還使用了originFrame來(lái)保存圖片的原始frame,因?yàn)槲覀儠?huì)用到這個(gè)值給圖片做一個(gè)動(dòng)畫使圖片充滿屏幕。所以應(yīng)該在點(diǎn)擊圖片的時(shí)候應(yīng)該記得保存圖片的原始frame,然后把它傳遞給動(dòng)畫控制器。
現(xiàn)在我們可以將注意力移到UIViewControllerAnimatedTransitioning的代理方法中。將返回時(shí)間的默認(rèn)0改為 return duration.
重復(fù)使用duration這個(gè)變量會(huì)讓我們更加方便的體驗(yàn)我們的轉(zhuǎn)場(chǎng)動(dòng)畫,我們可以通過(guò)簡(jiǎn)單的修改這個(gè)屬性的值來(lái)使得動(dòng)畫運(yùn)行的更快或者更慢。
設(shè)置我們的轉(zhuǎn)場(chǎng)動(dòng)畫上下文
現(xiàn)在是時(shí)候往我們的animateTransition方法里添加點(diǎn)魔法了。這個(gè)方法擁有一個(gè)UIViewControllerConextTransitioning類型的參數(shù),可以讓我們獲取到專場(chǎng)動(dòng)畫所需要的參數(shù)和viewControllers
在我們開(kāi)始代碼前,有必要知道什么是動(dòng)畫上下文(animation context)
當(dāng)兩個(gè)viewControllers之間的轉(zhuǎn)場(chǎng)動(dòng)畫開(kāi)始時(shí),當(dāng)前顯示的view會(huì)被添加到一個(gè)transition container view上,新的試圖控制器的view被創(chuàng)建了但是還沒(méi)有被看到,就像下面的視圖所展示的一樣

因此我們的任務(wù)就是用animateTransitin()方法將新的view添加到transition ?container 中
默認(rèn)狀態(tài)下,當(dāng)發(fā)生專場(chǎng)動(dòng)畫時(shí)舊視圖會(huì)從transition container中移除。如下圖:

我們需要先創(chuàng)建一個(gè)簡(jiǎn)單的專場(chǎng)動(dòng)畫來(lái)看看他是如何工作的,然后我們?cè)賹?shí)現(xiàn)更加炫酷、更加復(fù)雜的動(dòng)畫
先添加一個(gè)帶帶漸隱漸現(xiàn)效果的transition
我們先以一個(gè)簡(jiǎn)單的漸隱動(dòng)畫開(kāi)始,初步感受一下自定義的轉(zhuǎn)場(chǎng)動(dòng)畫。
在animateTransition:方法中添加以下代碼
OC:{
? ?UIView *containerView = transitionContext.containerView;
? ?UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
}
Swift{
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
}
首先我們獲取到了container view,我們的動(dòng)畫將會(huì)在它里面發(fā)生,然后我們還獲取到了新的view,用toView來(lái)代表
這個(gè)轉(zhuǎn)場(chǎng)上下文對(duì)象擁有兩個(gè)非常好用的方法,我們可以用這些方法獲取到一些有用的值
1、 - (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key方法。通過(guò)這個(gè)方法我們可以獲取到新舊view,比如通過(guò) UITransitionContextToViewKey可以獲取到新的view,通過(guò) UITransitionContextFromViewKey可以獲取到舊的view.
2、 - (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;通過(guò)這個(gè)方法可以獲取到新舊的視圖控制器,通過(guò) UITransitionContextFromViewControllerKey還有 UITransitionContextToViewControllerKey來(lái)獲取
此時(shí),我們同時(shí)擁有了container view和我們將要展示的view,接下來(lái)就是要將這個(gè)將要presented的view當(dāng)做containerViewde 子視圖添加到其上,并且以某種方式使其運(yùn)動(dòng)。
可以在animateTransition:方法里添加如下代碼
OC:{
? ?toView.alpha = 0;
? ?[UIView animateWithDuration:1.0 animations:^{
? ? ? ?toView.alpha = 1.0;
? ?}completion:^(BOOL finished) {
? ? ? ?[transitionContext completeTransition:YES];
? ?}];
}
Swift:{
containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration,
animations: {
toView.alpha = 1.0
},
completion: { _ in
transitionContext.completeTransition(true)
}
)
}
需要注意的是我們要在動(dòng)畫結(jié)束的時(shí)候用上下文調(diào)用completeTranstion方法,這會(huì)告訴我們的UIKit我們的動(dòng)畫完成了。
現(xiàn)在運(yùn)行我們的工程,點(diǎn)擊其中的一張圖片,會(huì)發(fā)現(xiàn)出現(xiàn)了漸顯式的動(dòng)畫。

* 現(xiàn)在我們已經(jīng)看到可以在animateTransition里做什么操作了,我們接下來(lái)添加點(diǎn)更有意思的東西。
添加一個(gè)帶Pop效果的transition
現(xiàn)在我們要重構(gòu)我們的代碼來(lái)獲得一個(gè)完全不一樣的新的轉(zhuǎn)場(chǎng)效果,先用如下的代碼替換掉原來(lái)animateTransition()中的代碼
OC代碼
【
UIView *containerView = transitionContext.containerView;
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *herbView = presenting?toView:[transitionContext viewForKey:UITransitionContextFromViewControllerKey];
】
Swift代碼
【
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let herbView = presenting ? toView :
?transitionContext.view(forKey: .from)!
】
containerView是我們動(dòng)畫發(fā)生的地方,toView是將要present出來(lái)的頁(yè)面。如果我們正在present新頁(yè)面,那么herbView就恰恰是toView,否則的話,就從上下文中獲取,無(wú)論是present還是dismiss,要確保herbView是我們要給添加動(dòng)畫的view。
當(dāng)我們present一個(gè)詳情頁(yè)時(shí),它會(huì)慢慢充滿整個(gè)屏幕,當(dāng)dismissed的時(shí)候,它會(huì)慢慢縮小回圖片的原有尺寸。
接下來(lái)向其中繼續(xù)添加如下代碼:
OC
【
? ?CGRect initialFrame = presenting?originrame:herbView.frame;
? ?CGRect finalFrame = presenting?herbView.frame :originrame;
? ?CGFloat xScaleFactor = presenting?initialFrame.size.width/finalFrame.size.width:finalFrame.size.width/initialFrame.size.width;
? ?CGFloat yScaleFactor = presenting?initialFrame.size.height/finalFrame.size.height:finalFrame.size.height/originrame.size.height;
】
Swift
【
let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame
let xScaleFactor = presenting ?
?initialFrame.width / finalFrame.width :
?finalFrame.width / initialFrame.width
let yScaleFactor = presenting ?
?initialFrame.height / finalFrame.height :
?finalFrame.height / initialFrame.height
】
在上面的代碼中,我們獲取到動(dòng)畫的初始frame信息以及最終的frame信息,然后計(jì)算出我們?cè)诿總€(gè)view上做動(dòng)畫時(shí)候的每條坐標(biāo)軸的縮放比例。
現(xiàn)在我們需要仔細(xì)的計(jì)算新的view的位置,使得它恰巧從我們所點(diǎn)擊的照片的上方出現(xiàn),這會(huì)使得這個(gè)動(dòng)畫看起來(lái)是我們點(diǎn)擊的這張照片變大了,最終填滿了整個(gè)屏幕。
繼續(xù)在animateTransition()中添加代碼
OC
【
? ?CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScaleFactor, yScaleFactor);
? ?if (presenting) {
? ? ? ?herbView.transform = scaleTransform;
? ? ? ?herbView.center = CGPointMake(initialFrame.origin.x, initialFrame.origin.y);
? ? ? ?herbView.clipsToBounds = YES;
? ?}
】
Swift
【
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?y: yScaleFactor)
if presenting {
?herbView.transform = scaleTransform
?herbView.center = CGPoint(
? ?x: initialFrame.midX,
? ?y: initialFrame.midY)
?herbView.clipsToBounds = true
}
】
當(dāng)我們present這個(gè)新的view的時(shí)候,我們?cè)O(shè)置了它的scale還有position。
現(xiàn)在我們?cè)诜椒ɡ锾砑幼詈髱仔写a
OC
【
? ?[containerView addSubview:toView];
? ?[containerView bringSubviewToFront:herbView];
? ?[UIView animateWithDuration:1.0f delay:0 usingSpringWithDamping:0.4 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
? ? ? ?herbView.transform = presenting?CGAffineTransformIdentity:scaleTransform;
? ? ? ?herbView.center = CGPointMake(finalFrame.origin.x, finalFrame.origin.y);
? ?} completion:^(BOOL finished) {
? ? ? ?[transitionContext completeTransition:YES];
? ?}];
】
Swift
【
containerView.addSubview(toView)
containerView.bringSubview(toFront: herbView)
UIView.animate(withDuration: duration, delay:0.0,
?usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,
?animations: {
? ?herbView.transform = self.presenting ?
? ? ?CGAffineTransform.identity : scaleTransform
? ?herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
?},
?completion:{_ in
? ?transitionContext.completeTransition(true)
?}
)
】
以上代碼首先會(huì)將toView添加到container上,然后下一步,我們需要確保herbView是在屏幕的頂部的,因?yàn)檫@是我們唯一讓之運(yùn)動(dòng)的view,應(yīng)該記住的是當(dāng)我們dismiss的時(shí)候,toView是我們的初始view,所以我們要把它加載其他任何東西的上面,如果不把它放在最上面的話我們的動(dòng)畫將會(huì)被隱藏掉。
然后我們可以開(kāi)始我們的動(dòng)畫了,使用帶spring 效果的動(dòng)畫會(huì)給它加上一點(diǎn)彈簧的效果。
在我們的動(dòng)畫表述中,我們改變了herbView的transform和position,當(dāng)present的時(shí)候,我們使其從底部的一個(gè)小尺寸變大到充滿整個(gè)屏幕。
此刻我們已經(jīng)將新的viewController搬到屏幕上來(lái)了,并且是在所點(diǎn)擊的圖片的上方,我們?cè)诔跏糵rame和最終的frame之間做動(dòng)畫,然后動(dòng)畫結(jié)束后,我們調(diào)用completeTransition方法向UIKit提交反饋。
然而,此時(shí)并不完美,但是只要我們稍加留意一下我們動(dòng)畫的幾個(gè)粗糙的地方,然后就會(huì)變成我們想要的樣子了。
現(xiàn)在我們的動(dòng)畫是從屏幕的左上角開(kāi)始的,因?yàn)閛riginFrame的默認(rèn)尺寸是(0,0),我們還沒(méi)有給它設(shè)置什么值。
打開(kāi)ViewController.Swift,向animationController(forPresented:)方法上面添加如下代碼
Swift
【
transition.originFrame =
selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
selectedImage!.isHidden = true
】
注意這里主要就是設(shè)置動(dòng)畫的初始位置,也就是我們所點(diǎn)擊的圖片的位置,OC代碼的話保持邏輯一致即可。