優雅地書寫 UIView 動畫

CrystleScho 8年前發布 | 7K 次閱讀 UIView Apple Swift開發

閉包成對出現時會惡心到你

Swift 代碼里的閉包是很好用的工具, 它們是一等公民, 如果他們在 API 的尾部時還可以變成尾隨閉包, 并且現在 Swift 3 里還 默認 為 noescape 以避免循環引用.

但每當我們不得不使用那些包含了多個閉包參數的API的時候, 就會讓這門優雅的語言變得很丑陋. 是的, 我說的就是你, UIView.

class func animate(withDuration duration: TimeInterval,            
    animations: @escaping () -> Void,          
    completion: ((Bool) -> Void)? = nil)

<!--more-->

尾隨閉包

UIView.animate(withDuration: 0.3, animations: {
    // 動畫
}) { finished in
    // 回調
}

我們正在混合使用多個閉包和尾隨閉包, animation: 還擁有參數標簽, 但 completion: 已經丟掉參數標簽變成一個尾隨閉包了. 在這種情況下, 我覺得尾隨閉包已經跟原有的函數產生了割裂感, 但我猜這是因為 API 的右尖括號跟右括號讓我感覺這個函數已經結束了:

}) { finished in // 糟透了

縮進之美

另一個就是 animation 的兩個閉包是同一層級的, 而它們默認的縮進卻不一致. 最近我感受了一下函數式編程的偉大, 寫函數式代碼的一個很爽的點在于把那些序列的命令一條一條通過點語法羅列出來:

[0, 1, 2, 4, 5, 6]
    .sorted { $0 < $1 }
    .map { $0 * 2 }
    .forEach { print($0) }

那為什么不能把帶兩個閉包的 API 用同樣的方式列出來?

把丑陋的語法強制變得優雅

UIView.animate(withDuration: 0.3,
    animations: {
        // 動畫
    },
    completion: { finished in
        // 回調
    })

我想借鑒一下函數式編程的語法, 強迫自己去手動調整代碼格式而不是用 Xcode 默認的自動補齊. 我個人覺得這樣子會讓代碼可讀性更加好但這也是一個很機械性的過程. 每次我復制粘貼這段代碼的時候, 縮進總是會亂掉, 但我覺得這是 Xcode 的問題而不是 Swift 的.

傳遞閉包

let animations = {
    // 動畫
}
let completion = { (finished: Bool) in
    // 回調
}
UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)

這篇文章開頭我提到閉包是Swift 的一等公民, 這意味著我們可以把它賦值給一個變量并且傳遞出去. 我覺得這么寫并不比上一個例子更具可讀性, 而且別的對象只要想要就可以去接觸到這些閉包. 如果一定要我選擇的話, 我更樂意使用上一種寫法.

解決方案

就像許多程序員一樣, 我會強迫自己去思考出一個方式去解決這個很常見的問題, 并且告訴自己, 長此以往我可以節省很多時間.

UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()

就像你看到的, 這種語法和結構從 Swift 函數式的 API 里借鑒了很多. 我們把兩個閉包的看作是集合的高等函數, 然后現在代碼看起來好很多, 并且在我們換行和復制粘貼的時候, 編譯器也會根據我們想要的那樣去工作(譯者注: 這應該跟 IDE 的 formator 有關, 而不是編譯器, 畢竟 Swift 不需要游標卡尺:joy:)

"長此以往我可以節省很多時間"

Animator

class Animator {
    typealias Animations = () -> Void
    typealias Completion = (Bool) -> Void
    private var animations: Animations
    private var completion: Completion?
    private let duration: TimeInterval
    init(duration: TimeInterval) {
        self.animations = {} // 譯者注: 把 animation 聲明為 ! 的其實就可以省略這一行
        self.completion = nil // 這里其實也是可以省略的
        self.duration = duration
    }
...

這里的 Animator 類很簡單, 只有三個成員變量: 一個動畫時間和兩個閉包, 一個初始化構造器和一些函數, 待會我們會講一下這些函數的作用. 我們已經用了一些 typealias 提前定義一些閉包的簽名, 但這是一個提高代碼可讀性的好習慣, 并且如果我們在多個地方用到了這些閉包, 需要修改的時候, 只需要修改定義, 編譯器就會替我們找出所有需要調整的地方, 而不是由我們自己去把所有實現都給找出來, 這樣就可以幫助我們減少出錯的幾率.

這些閉包變量是可變的(用 var 聲明), 所以我們需要把他們保存在某個地方, 并且在實例化之后去修改它, 但同時他們也是 private 私有的, 避免外部修改. completion 是 optional 的, 而 animation 不是, 就像 UIView 的官方 API 那樣. 在我們初始化構造器的實現里, 我們給閉包一個默認值避免編譯器報錯.

func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}
func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}

閉包集合的實現非常簡單, 接受一個閉包的參數, 然后把它賦值給相應的變量就行了.

返回 Self

最棒的一點是, 這些 API 都會把返回自己, 這樣我們就可以鏈式地調用:

let numbers =
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $1 } // Returns Array
    .map { $0 * 2 }     // Returns Array

然而, 如果鏈式調用的最后一個函數返回一個對象, 那我們就可以把它賦值給某個變量, 然后繼續使用, 在這里我們把結果賦值給了 numbers.

而如果函數返回空值那我們就不必賦值給變量了:

[0, 1, 2, 4, 5, 6]         // Returns Array
    .sorted { $0 < $0 }    // Returns Array
    .map { $0 * 2 }        // Returns Array
    .forEach { print($0) } // Returns Void

Animating

func animate() {
    UIView.animate(withDuration: duration,
        animations: animations,
        completion: completion)
}

就像函數式一樣, 前面所有的調用都是為了最后的結果, 這并不是一件壞事. Swift 允許我們作為思考者, 工匠和程序員去重新想象和構建我們所需要的工具.

擴展 UIView

extension UIView {
    class Animator { ...

最后, 我們把 Animator 的放到 UIView 的 extension 里, 主要是因為 Animator 是強依賴于 UIView 的, 并且內部函數需要獲取到 UIView 內部的上下文, 我們沒有任何必要把它獨立成一個類.

Options

UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])

還有一些參數是我們需要傳遞給 animation 的 API 里的,我們還可以繼承 Animator 類再創建一個 SpringAnimator 去滿足我們日常的絕大部分需求.

就像之前那樣, 我提供了一個 playgrounds 在 Github 上, 或者看一下這里的 Gist 也可以, 這樣你就不必打開 Xcode 了.

大家有沒有印象 URLRequest 的寫法, 典型的寫法是這樣子的:

let url = URL()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    // 回調
}
task.resume()

剛接觸這個 API 的時候, 我經常忘記書寫后面那句 task.resume() , 雖然這么寫很 OO, 但是我還是很討厭這種寫法, 因為生活中任務不是一個可命令的對象, 我命令這個任務執行是一件很違反直覺的事情

同樣的, 我也不太喜歡原文里最后的那一句 animate , 所以我們可以用 promise 的思路去寫:

class Animator {
    typealias Animations = () -> Void
    typealias Completion = (Bool) -> Void

private let duration: NSTimeInterval

private var animations: Animations! {
    didSet {
        UIView.animateWithDuration(duration, animations: animations) { success in
            self.completion?(success)
            self.success = success
        }
    }
}
private var completion: Completion? {
    didSet {
        guard let success = success else { return }
        completion?(success)
    }
}

private var success: Bool?

init(duration: NSTimeInterval) {
    self.duration = duration
}

func animations(animations: Animations) -> Self {
    self.animations = animations
    return self
}

func completion(completion: Completion) -> Self {
    self.completion = completion
    return self
}

}</code></pre>

我把原有的 animate 函數去掉了, 加了一個 success 變量去保存 completion 回調的參數.

這里會有兩種情況: 一種是動畫先結束, completion 還沒被賦值, 另一種情況是 completion 先被賦值, 動畫還沒結束. 我的代碼可能有一點點繞, 主要是利用了 Optional chaining 的特性, completion 其實只會執行一次.

稍微思考一下或者自己跑一下大概就能理解了, 這里其實我也只是簡單的處理了一下時序問題, 并不完美, 還是有極小的概率會出問題, 但鑒于動畫類 API 的特性, 兩個閉包都會按順序跑在主線程上, 而且時間不會設的特別短, 所以正常情況是不會出問題

具體調用起來會是這個樣子, 這個時候再把這個類命名為 Animator 其實已經不是很適合:

UIView.Animator(duration: 3)
    .animations {
        // 動畫
    }
    .completion {
        // 回調
    }

雖然只是少了一句代碼, 但是我覺得會比之前更好一點, 借用作者的那句話 "save time in the long run"

 

來自:http://www.jianshu.com/p/9276e6941d5c

 

 本文由用戶 CrystleScho 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!