个性化阅读
专注于IT技术分析

iOS中的RxSwift和动画

本文概述

如果你是一位iOS开发人员, 并且完成了相当多的UI工作并且对此充满热情, 那么你就一定会喜欢UIKit在动画方面的强大功能。动画UIView就像蛋糕一样容易。你无需考虑如何使其随时间逐渐褪色, 旋转, 移动或收缩/扩展。但是, 如果你要将动画链接在一起并在它们之间建立依赖关系, 则会涉及到一点。你的代码可能最终变得非常冗长且难以遵循, 并带有许多嵌套的闭包和缩进级别。

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

响应式编程简介

响应式编程正成为一种主流, 并且已被大多数现代编程语言所采用。那里有许多书籍和博客详细解释了为什么反应式编程如此强大, 以及如何通过执行某些设计原理和模式来帮助鼓励良好的软件设计。它还提供了一个工具包, 可以帮助你大大减少代码混乱。

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

在谈到Swift时, 有两个相互竞争的框架可帮助你将其转变为一种反应式编程语言:ReactiveSwift和RxSwift。我在示例中使用RxSwift并不是因为它更好, 而是因为我对它更加熟悉。我将假定你(读者)也熟悉它, 以便我可以直接了解它的实质。

链接动画:旧方法

假设你想将视图旋转180度, 然后使其淡出。你可以利用完成闭包并执行以下操作:

       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旋转,暂停然后向右滑动

添加到它的步骤越多, 它就会变得越发交错和麻烦。然后, 如果你决定更改某些步骤的顺序, 则必须执行一些非平凡的剪切和粘贴序列, 这很容易出错。

好吧, 苹果公司显然已经想到了这一点-他们提供了一种更好的方式, 使用基于关键帧的动画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并不是唯一可以用来执行类似操作的框架-任何基于诺言的框架都可以将异步操作包装成可以在语法上链接在一起的方法, 而无需使用完成块。但是RxSwift的一系列运算符可以提供更多的功能, 我们稍后会介绍。

这是我将如何做的概述:

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

这是我的函数的样子:

    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的” Reactive”扩展中(可通过.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的建议, 请尝试阅读由srcminier Roman Stetsenko撰写的”如何实现像素完美的iOS UI设计”。


原始记录(了解基础知识)

  • Wikipedia-Swift(编程语言)
  • Lynda.com-RxSwift:iOS开发人员的设计模式
  • We Heart Swift-UIView基础知识
  • 角度-可观察
赞(0)
未经允许不得转载:srcmini » iOS中的RxSwift和动画

评论 抢沙发

评论前必须登录!