英语原文地址:RxSwift and Animations in iOS - VADIM DAGMAN
如果你是一个对UI界面充满激情的 iOS 开发者,当谈及动画的时候,你会感受到 UIKit 的强大。让一个UIView 执行动画像做蛋糕一样简单。你不需要去想太多的关于随着时间变透明,旋转,移动或者晃动/伸展。但是, 如果你想将动画连接在一起,建立它们之间的依赖关系,代码可能会因许多嵌套的闭包和缩进变的相当冗长,难以理解。
这篇文章,我将探讨如果运用响应式框架例如 RxSwift, 使代码看起来更加的干净,更好的阅读和理解。这个想法出现源于一个客户的一个项目,一个对UI非常挑剔的客户(完全匹配我的激情),他想让他 app 的 UI 以一种非常特别的方式展示,拥有很多非常圆滑的过渡和动画。其中他们的一个想法就是像讲故事一样介绍他们 APP,他们想让整个故事通过一连串的动画去叙述,同时也可以很容易的进行调整和优化,而不是通过播放一个预先定义好的视频。RxSwift 就是一个完美的选择对于这个问题,因此我希望你读完这篇文章能感受得到。
响应式编程成为主要的和运用于大多数现代编程语言。为什么响应式编程是一个强大的概念,有大量的书和博客进行了详细的解释, 以及它的设计原则和设计模式是如何有助于鼓励好的软件去执行。同时它也是一个工具,可以帮助你大大减少代码杂乱。
我非常喜欢其中的一个方面—就是你可以通过一个非常简单和易读的方式去连接很多异步操作并传递他们。
在 swift 中, 有两个相互竞争的框架帮你实现响应式编程:ReactiveSwift 和 RxSwift,我将使用 RxSwift 并不是因为它更好,而是因为我对它更加的熟悉,我将假设你以及读者也熟悉它, 这样我就可以直接介绍和使用它。
如果你想180度旋转一个 view,然后褪色消失,你可能使用 UIView animation completion
来实现
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.alpha = 0
})
})
虽然有一点笨重,但也仍然可行。但是如果你想添加更多的动画在里面,比如说在旋转中消失前移动视图?运用同样的方法,将会这样写
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: 100, dy: 0)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.alpha = 0
})
})
})
添加的步骤越多,代码越交错,麻烦也越多。如果你想改变某些步骤的顺序,你必须执行一些简单的剪切和粘贴序列,这是容易出错的。
庆幸的是,苹果提供一个更好的方法,使用keyframe-based animations 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: 100, dy: 0)
})
UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: {
self.animatableView.alpha = 0
})
})
这是一个巨大的进步,主要优点是:
这种方法的缺点是,改变顺序,你必须考虑相对时间,变得很困难(或至少不是非常简单的)。想想计算你会经历什么,你需要做什么样的修改, 总体持续时间和相对时间/开始时间为每个动画, 如果你决定让视图1秒内消失,而不是半秒,同时保持相同的一切。同样如果你想改变步骤的顺序必须验算相对的开始时间。
考虑到缺点,我认为上述方法并不够好。
我要找理想的解决方案应该满足以下目标:
我发现使用RxSwift,我可以很容易地实现这两个目标。
RxSwift不是唯一的框架,你可以使用类似的,任何类似的框架,只要让你用异步操作方法但没有利用闭包连在在一起完成就可以了。
但 RxSwift 提供了非常多的操作符, 我们将在后面接触一点。
这里是我想要实现的想法:
Observable
flatMap
操作符进行连接下面就是我的函数:
func rotate(_ view: UIView, duration: TimeInterval) -> Observable {
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 {
return Observable.create({ (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.frame = view.frame.offsetBy(dx: 100, dy: 0)
}, completion: { _ in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
})
}
func fade(_ view: UIView, duration: TimeInterval) -> Observable {
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 {
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 {
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
操作符中的值
现在,我们进行更高级的封装来实现动画功能。将 动画添加到 Reactive
扩展中(通过.rx后缀来访问),这将使它成为 RxSwift的实例, 响应式编程函数通常是通过.rx
后缀来完成,返回一个可观察对象,是代码更加的清晰明了。
extension Reactive where Base == UIView {
func shift(duration: TimeInterval) -> Observable {
return Observable.create({ (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
self.base.frame = self.base.frame.offsetBy(dx: 100, dy: 0)
}, completion: { _ in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
})
}
func fade(duration: TimeInterval) -> Observable {
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 {
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)
over!!!