1. 程式人生 > >Swift 傻瓜技巧 #6:有動畫或無動畫

Swift 傻瓜技巧 #6:有動畫或無動畫

作者:Wooji Juice,原文連結,原文日期:2018-11-14 譯者:石榴;校對:numbbbbbCee;定稿: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() 或其他方法。

以上都是相比於之前改進的地方,但也不是沒有問題。尤其是它開始變得有點囉嗦:

  1. doStuff(with: ...) 需要寫入一個很長的 UIViewPropertyAnimator 建構函式。不是很理想,不過跟下面比起來不算什麼:
  2. 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。

(注意 animatoraddCompletion 之間沒有 ?

如果有 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