優雅地書寫 UIView 動畫

原文: Swift: UIView Animation Syntax Sugar
做者: Andyy Hope
譯者: kemchenjgit

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

Swift 代碼裏的閉包是很好用的工具, 它們是一等公民, 若是他們在 API 的尾部時還能夠變成尾隨閉包, 而且如今 Swift 3 裏還默認noescape 以免循環引用.程序員

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

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

尾隨閉包

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

咱們正在混合使用多個閉包和尾隨閉包, animation: 還擁有參數標籤, 但 completion: 已經丟掉參數標籤變成一個尾隨閉包了. 在這種狀況下, 我以爲尾隨閉包已經跟原有的函數產生了割裂感, 但我猜這是由於 API 的右尖括號跟右括號讓我感受這個函數已經結束了:編程

}) { finished in // 糟透了

若是你不肯定什麼是尾隨閉包, 我有另外一篇文章解釋它的定義和用法 Swift: Syntax Cheat Codesswift

縮進之美

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

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

那爲何不能把帶兩個閉包的 API 用一樣的方式列出來?promise

若是你不理解 $0 語法, 我有另外一篇文章介紹如何它們的含義和語法 Swift: Syntax Cheat Codes閉包

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

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

我想借鑑一下函數式編程的語法, 強迫本身去手動調整代碼格式而不是用 Xcode 默認的自動補齊. 我我的以爲這樣子會讓代碼可讀性更加好但這也是一個很機械性的過程. 每次我複製粘貼這段代碼的時候, 縮進老是會亂掉, 但我以爲這是 Xcode 的問題而不是 Swift 的.app

傳遞閉包

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

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

解決方案

就像許多程序員同樣, 我會強迫本身去思考出一個方式去解決這個很常見的問題, 而且告訴本身, 久而久之我能夠節省不少時間.

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

就像你看到的, 這種語法和結構從 Swift 函數式的 API 裏借鑑了不少. 咱們把兩個閉包的看做是集合的高等函數, 而後如今代碼看起來好不少, 而且在咱們換行和複製粘貼的時候, 編譯器也會根據咱們想要的那樣去工做(譯者注: 這應該跟 IDE 的 formator 有關, 而不是編譯器, 畢竟 Swift 不須要遊標卡尺?)

"久而久之我能夠節省不少時間"

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 了.

若是你喜歡這篇文章的話, 也能夠看一下我別的文章, 或者你想在你的項目裏使用這個方法的話, 請在 Twitter 上發個推@我或者關注我, 這都會讓我很開心.

譯者言

翻譯這篇文章的時候, 我很偶然地在簡書上看到了 Cyandev 的 Swift 中實現 Promise 模式 (我很喜歡他寫的文章), 發現其實能夠再優化一下

你們有沒有印象 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
    }
}

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

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

稍微思考一下或者本身跑一下大概就能理解了, 這裏其實我也只是簡單的處理了一下時序問題, 並不完美, 仍是有極小的機率會出問題, 但鑑於動畫類 API 的特性, 兩個閉包都會按順序跑在主線程上, 並且時間不會設的特別短, 因此正常狀況是不會出問題

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

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

雖然只是少了一句代碼, 可是我以爲會比以前更好一點, 借用做者的那句話 "save time in the long run"

相關文章
相關標籤/搜索