优雅地书写 UIView 动画

原文: Swift: UIView Animation Syntax Sugar
作者: Andyy Hope
译者: kemchenj

闭包成对出现时会恶心到你

Swift 代码里的闭包是很好用的工具, 它们是一等公民, 如果他们在 API 的尾部时还可以变成尾随闭包, 并且现在 Swift 3 里还默认noescape 以避免循环引用.

但每当我们不得不使用那些包含了多个闭包参数的API的时候, 就会让这门优雅的语言变得很丑陋. 是的, 我说的就是你, UIView.

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 Codes

缩进之美

另一个就是 animation 的两个闭包是同一层级的, 而它们默认的缩进却不一致. 最近我感受了一下函数式编程的伟大, 写函数式代码的一个很爽的点在于把那些序列的命令一条一条通过点语法罗列出来:

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

那为什么不能把带两个闭包的 API 用同样的方式列出来?

如果你不理解 $0 语法, 我有另一篇文章介绍如何它们的含义和语法 Swift: Syntax Cheat Codes

把丑陋的语法强制变得优雅

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 不需要游标卡尺😂)

"长此以往我可以节省很多时间"

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"

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容