iOS 中的 RxSwift 和动画

已发表: 2022-03-11

如果你是一个 iOS 开发者,做了一些合理的 UI 工作并且对它充满热情,那么你一定会喜欢 UIKit 在动画方面的强大功能。 动画 UIView 就像蛋糕一样简单。 您不必考虑如何让它随着时间的推移而褪色、旋转、移动或收缩/扩展。 但是,如果您想将动画链接在一起并设置它们之间的依赖关系,则会涉及一些问题。 您的代码最终可能会变得非常冗长且难以理解,其中包含许多嵌套闭包和缩进级别。

在本文中,我将探讨如何应用 RxSwift 等响应式框架的强大功能,使代码看起来更简洁,更易于阅读和遵循。 当我为一个客户做一个项目时,我产生了这个想法。 那个特定的客户非常精通 UI(这完全符合我的热情)! 他们希望他们的应用程序的 UI 以一种非常特殊的方式运行,具有大量非常流畅的过渡和动画。 他们的想法之一是对应用程序进行介绍,以讲述该应用程序的内容。 他们希望通过一系列动画来讲述这个故事,而不是通过播放预渲染的视频,以便可以轻松地对其进行调整和调整。 事实证明,RxSwift 是解决此类问题的完美选择,我希望您在阅读完本文后会意识到这一点。

反应式编程简介

反应式编程正在成为一种主要内容,并已被大多数现代编程语言采用。 有很多书籍和博客非常详细地解释了为什么响应式编程是一个如此强大的概念,以及它如何通过强制执行某些设计原则和模式来帮助鼓励良好的软件设计。 它还为您提供了一个工具包,可以帮助您显着减少代码混乱。

我想谈谈我真正喜欢的一个方面——您可以轻松地链接异步操作并以声明性、易于阅读的方式表达它们。

谈到 Swift,有两个相互竞争的框架可以帮助您将其转变为一种反应式编程语言:ReactiveSwift 和 RxSwift。 我将在我的示例中使用 RxSwift 不是因为它更好,而是因为我更熟悉它。 我假设你,读者,也熟悉它,这样我就可以直接进入它的实质。

链接动画:旧方式

假设您要将视图旋转 180°,然后将其淡出。 您可以使用completion闭包并执行以下操作:

 UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) 

旋转正方形的 GIF 动画

它有点笨重,但还可以。 但是如果你想在两者之间再插入一个动画,比如在视图旋转之后和消失之前将视图向右移动怎么办? 应用同样的方法,你最终会得到这样的结果:

 UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.alpha = 0 }) }) }) 

矩形旋转、暂停然后向右滑动的 GIF 动画

您添加的步骤越多,它就越交错和繁琐。 然后,如果您决定更改某些步骤的顺序,您将不得不执行一些重要的剪切和粘贴序列,这很容易出错。

好吧,Apple 显然已经想到了这一点——他们提供了一种更好的方法,使用基于关键帧的动画 API。 使用这种方法,上面的代码可以这样重写:

 UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }) UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }) UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: { self.animatableView.alpha = 0 }) })

这是一个很大的改进,主要优点是:

  1. 无论您添加多少步骤,代码都保持不变
  2. 更改顺序很简单(下面有一个警告)

这种方法的缺点是您必须考虑相对持续时间,并且很难(或至少不是很简单)更改步骤的绝对时间或顺序。 想想如果您决定让视图在 1 秒内而不是半秒内淡出,您将必须进行哪些计算,以及您必须对每个动画的总体持续时间和相对持续时间/开始时间进行哪些更改第二,同时保持其他一切不变。 如果你想改变步骤的顺序也是一样的——你必须重新计算它们的相对开始时间。

鉴于这些缺点,我发现上述任何一种方法都不够好。 我正在寻找的理想解决方案应满足以下条件:

  1. 无论步数如何,代码都必须保持平坦
  2. 我应该能够轻松地添加/删除或重新排序动画并独立更改其持续时间,而不会对其他动画产生任何副作用

链接动画:RxSwift 方式

我发现使用 RxSwift,我可以轻松实现这两个目标。 RxSwift 不是你可以用来做类似事情的唯一框架——任何基于 Promise 的框架都可以让你将异步操作包装到可以在语法上链接在一起而不使用完成块的方法中。 但是 RxSwift 提供了更多的操作符数组,我们稍后会谈到。

以下是我将如何做到这一点的大纲:

  1. 我会将每个动画包装到一个函数中,该函数返回一个Observable<Void>类型的可观察对象。
  2. 在完成序列之前,该 observable 将只发出一个元素。
  3. 该元素将在函数包装的动画完成后立即发出。
  4. 我将使用flatMap运算符将这些 observable 链接在一起。

这就是我的函数的样子:

 func rotate(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func shift(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.frame = view.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } }

这是我如何将它们组合在一起的:

 rotate(animatableView, duration: 0.5) .flatMap { [unowned self] in self.shift(self.animatableView, duration: 0.5) } .flatMap { [unowned self] in self.fade(self.animatableView, duration: 0.5) } .subscribe() .disposed(by: disposeBag)

它肯定比以前的实现要多得多的代码,对于这样一个简单的动画序列来说可能看起来有点矫枉过正,但它的美妙之处在于它可以扩展以处理一些非常复杂的动画序列,并且非常容易阅读,因为语法的声明性。

一旦你掌握了它,你就可以创建像电影一样复杂的动画,并且可以使用大量方便的 RxSwift 运算符来完成使用上述任何方法都很难完成的事情。

以下是我们如何使用.concat运算符使我的代码更加简洁——动画被链接在一起的部分:

 Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

您可以在动画之间插入延迟,如下所示:

 func delay(_ duration: TimeInterval) -> Observable<Void> { return Observable.of(()).delay(duration, scheduler: MainScheduler.instance) } Observable.concat([ rotate(animatableView, duration: 0.5), delay(0.5), shift(animatableView, duration: 0.5), delay(1), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

现在,假设我们希望视图在开始移动之前旋转一定次数。 我们想轻松地调整它应该旋转多少次。

首先,我将创建一个连续重复旋转动画并在每次旋转后发出一个元素的方法。 我希望这些旋转在可观察对象被处理后立即停止。 我可以做这样的事情:

 func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { view.transform = view.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } }

然后我美丽的动画链可能看起来像这样:

 Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

您会看到控制视图旋转的次数是多么容易——只需更改传递给take运算符的值。

改进动画的动画 GIF

现在,我想通过将我创建的每个动画函数包装到 UIView 的“反应式”扩展(可通过.rx后缀访问)中,使我的实现更进一步。 这将使它更符合 RxSwift 约定,其中反应函数通常通过.rx后缀访问,以明确它们返回一个可观察对象。

 extension Reactive where Base == UIView { func shift(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func rotateEndlessly(duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { self.base.transform = self.base.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } } }

有了这个,我可以像这样把它们放在一起:

 Observable.concat([ animatableView.rx.rotateEndlessly(duration: 0.5).take(5), animatableView.rx.shift(duration: 0.5), animatableView.rx.fade(duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

从这往哪儿走

正如这篇文章所展示的,通过释放 RxSwift 的力量,一旦你有了原语,你就可以享受动画带来的乐趣。 您的代码干净且易于阅读,并且不再像“代码”——您“描述”了您的动画是如何组合在一起的,它们就变得生动起来! 如果你想做比这里描述的更精细的事情,你当然可以通过添加更多你自己封装其他类型动画的原语来做到这一点。 您还可以随时利用开源社区中一些热情的人开发的不断增长的工具和框架库。

如果您是 RxSwift 转换者,请定期访问 RxSwiftCommunity 存储库:您总能找到新的东西!

如果您正在寻找更多 UI 建议,请尝试阅读 Toptaler Roman Stetsenko的 How to Implement a Pixel-perfect iOS UI Design


源注释(了解基础)

  • 维基百科 - Swift(编程语言)
  • Lynda.com - RxSwift:iOS 开发者的设计模式
  • We Heart Swift - UIView 基础知识
  • Angular - Observables