作者:Wooji Juice,原文連結,原文日期:2018-11-14 譯者:石榴;校對:numbbbbb,Cee;定稿:Forelax
流暢的動畫一開始就被認為是 iOS 應用的特點之一。這不僅歸功於 iOS 系統強大的動畫引擎(從而使得 App 能夠一邊展示流暢的動畫一邊做著其他的事情),還歸功於系統提供的非常方便的動畫 API:
// 無動畫
doStuff()
// 有動畫
UIView.animate(withDuration: 1) { doStuff() }
複製程式碼
只需要將你的程式碼放進 block(閉包)中,就可以讓它們擁有流暢的緩入緩出的動畫效果。
然而,如果你使用過這套系統,你可能會遇到一些問題。這個系統可以完美地處理簡單的情況,比如讓一個東西淡入、淡出,或改變它的顏色,但在更復雜的情況下,這種方法就會開始出現問題。
例如下面這個例子,你想要淡出一個元素,然後刪除它。UIView
支援這種操作:
UIView.animate(withDuration: 1, animations:
{
someting.alpha = 0
}, completion:
{
something.removeFormSuperView()
})
複製程式碼
但你只能把所有東西都寫在 completion
block 裡時才會工作。在大型專案中,我們需要把複雜的任務拆解成小的方法。但問題就在這些方法中,像在上個例子中的 doStuff()
,我們無法在 completion
block 中新增程式碼。
我們也無法得知動畫有多長(甚至都不知道有沒有動畫),所以如果我們沒有辦法簡單地和動畫時間之間同步(如在 一個音訊編輯軟體 中讓進度條同步前進)。
總的來說,我們無法獲知關於動畫的資訊,他們僅僅是執行程式碼,進行或不進行動畫,並不會受我們控制。
如果我們在檢視中新增帶有 Auto Layout 的新元素,事情就會變得更復雜:你需要小心地呼叫 UIView.performWithoutAnimation { }
,否則新出現的檢視就會從 (x: 0, y: 0, w: 0, h: 0)
瞬移到它們的目標位置。
檢視屬性 Animator
很長時間以來,我一直在改變程式碼中動畫的寫法。最開始我寫了我自己的 AnimationContext
類來協助,後來蘋果提供了他們功能相同的 UIViewPropertyAnimator
,現在我會在所有可能的地方使用它。
一般來說,我發現最有效的方法是寫一個「可動畫」的方法並顯式接受一個 animator 引數:
func doStuff(with animator: UIViewPropertyAnimator? = nil)
{
// ...
}
複製程式碼
之後我就可以直接呼叫 doStuff()
不新增動畫並完成任務,或呼叫 doStuff(with: UIViewPropertyAnimator(duration: 1, curve: .easeInOut))
或加其他的引數去完成任務並新增動畫。
(實際情況中,上述方法通常會被稱作 reflectCurrentState()
或其他特定領域的名字;該方法執行所有必要的修改,並將檢視與最新的資料同步。該方法一般不會被本檢視以外的程式碼呼叫,而是被檢視自己呼叫,然後會根據需要繼續呼叫其他內部方法,或將 animator 傳給其他內部方法。不過這不在本文的討論範圍內。)
doStuff()
可以像之前一樣,帶有或不帶有動畫執行一個任務。但現在它帶有了更多資訊:它知道自己是否執行動畫;它可以讀取 animator 的 duration
屬性(如果有的話)。他可以呼叫 animator 的 addAnimation
來明確地指定哪些程式碼需要動畫,並直接執行不需要動畫的程式碼;他可以呼叫 addCompletion
來處理 removeFromSuperView()
或其他方法。
以上都是相比於之前改進的地方,但也不是沒有問題。尤其是它開始變得有點囉嗦:
doStuff(with: ...)
需要寫入一個很長的UIViewPropertyAnimator
建構函式。不是很理想,不過跟下面比起來不算什麼:- 在
doStuff()
內部,需要檢查UIViewPropertyAnimator
是否存在並調整程式碼。
我們不能簡單的依賴 optional chaining(可選鏈式呼叫)(如 animator?.addcompletion { something.removeFromSuperview() }
),因為如果 animator 是 nil
會導致 block 中的程式碼被直接跳過,然而無論有沒有動畫,我們都希望該檢視在父檢視中被移除。
為了保證正確的行為,你的程式碼會類似這個樣子:
func doStuff(with animator: UIViewPropertyAnimator? = nil)
{
if let animator = animator
{
_ in something.removeFromSuperview()
}
else
{
something.removeFromSuperview()
}
}
複製程式碼
Objective-C 愛好者即使瞧不起 Optional(可選)也笑不出來 -- 使用 Objective-C 也不會改善這種情況:
- (void) doStuffWithAnimator: (nullable UIViewPropertyAnimator *) animator
{
if (animator != nil)
{
[animator addCompletion: ^(UIViewAnimatingPosition position)
{
[something removeFromSuperview];
}];
}
else
{
[something removeFromSuperView];
}
}
複製程式碼
一旦你在生產環境中想使用這樣的程式碼,你最終會寫出更雜亂、更難於閱讀和維護的程式碼。
幸運的是,我們可以進一步的改進這段程式碼。
Optional 不是 Nil
的另一個叫法
改進這段程式碼的訣竅就在於,UIViewPropertyAnimator
在這裡是 Optional,關鍵點就在於 Optional 在 Swift 中的意義。
有的時候人們會抱怨 Swift 的 Optional 非常煩人,因為在 Objective-C 中(Objective-C 中使用 nil
指標來替代 Swift 中的 Optional)你可以直接對指標呼叫方法。
Objective-C 不會抱怨指標是不是 nil
:如果指標非空,方法會直接被呼叫;如果是空指標,呼叫會被無聲地忽略掉,不用程式設計師做其他的事情。
我不同意這個意見。在 Swift 中,在你知道你在做什麼的情況下,你只需要加一個 ?
,並不是一個很大的負擔。但是由於有了 Swift 的 Optional,我們可以做更多事情。
因為在 Swift 中,Optional 是一個“真實的東西”,而不是“缺少的東西”。無論一個 Optional 的值是什麼,就算是 nil
,它也是一個列舉值,你可以對它呼叫方法,呼叫的方法也會被執行。講真的,Swift 的列舉超級好用!
(有趣的是,在 Objective-C 類中對 Swift 的 nil
的底層表示就是空指標,所以它們的效率還是很高的。但是語法層面,它們非常的不同。我們會在接下來利用這個性質。)
因為在 Swift 中,你可以對幾乎所有型別新增擴充,不僅僅是 Objective-C 類。你可以:
extension Optional where Wrapped == UIViewPropertyAnimator
{
@discardableResult
func addCompletion(_ block: @escaping (UIViewAnimatingPosition)->()) -> Optional<UIViewPropertyAnimator>
{
if let animator = self
{
animator.addCompletion(block)
}
else
{
block(.end)
}
return self
}
}
複製程式碼
這段程式碼將難看的程式碼移動到了 Optional 的庫中(但只針對 UIViewPropertyAnimator
)。現在,你的檢視可以:
func doStuff(with animator: UIViewPropertyAnimator? = nil)
{
animator.addCompletion { _ in something.removeFromSuperview() }
}
複製程式碼
現在回撥函式總會被執行,無論有沒有 animator。
(注意 animator
和 addCompletion
之間沒有 ?
)
如果有 animator,block 中的程式碼會在動畫完成時被呼叫;如果沒有 animator,block 中的程式碼會被立即呼叫,因為 nil
Optional 仍然是 Optional,擁有所有 Optional 的方法,當然也包括我們剛剛新增的方法 -- 而不是一個吞下所有的滾落到它表面的方法呼叫的黑洞。
我還有類似的擴充方法來執行總是需要被執行的任務,有些是動畫的一部分,或其他的立即執行的程式碼:如果我想讓一個元素緩入,我會在把元素放入檢視之前將 alpha 值設定成 0,然後呼叫 animator.perform { something.alpha = 1 }
來保證它無論有沒有動畫都會變得可見。
與 Optional 無關,我還在 UIViewPropertyAnimator
中新增了一些靜態方法來生成一些常見的動畫,如:static func spring(...)
、static func linear(...)
。Swift 的名稱解析方法決定了你可以寫出更簡潔的程式碼,如:doStuff(with: .spring(duration: 1))
。
當然,以上只是一些小的程式碼技巧,而不是重新構想程式碼或應用結構。但是隨著專案的複雜度增加,像這種小的改進也會疊加起來,幫助我們對抗不斷增加的複雜度,維持大型專案的可控性。謝謝你,Swift。Thwift.
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg。