英語原文地址: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、不管有多少步驟添加, 代碼不變
- 2、改變動畫的順序也是非常簡單的
這種方法的缺點是,改變順序,你必須考慮相對時間,變得很困難(或至少不是非常簡單的)。想想計算你會經歷什麼,你需要做什麼樣的修改, 總體持續時間和相對時間/開始時間爲每個動畫, 如果你決定讓視圖1秒內消失,而不是半秒,同時保持相同的一切。同樣如果你想改變步驟的順序必須驗算相對的開始時間。
考慮到缺點,我認爲上述方法並不夠好。
我要找理想的解決方案應該滿足以下目標:
- 1、代碼已經持平不管多少數量的步驟
- 2、我應該能夠輕鬆地添加/刪除或重新排序獨立動畫和改變他們的持續時間,對其他動畫沒有任何副作用
鏈式動畫:使用 RxSwift
我發現使用RxSwift,我可以很容易地實現這兩個目標。
RxSwift不是唯一的框架,你可以使用類似的,任何類似的框架,只要讓你用異步操作方法但沒有利用閉包連在在一起完成就可以了。
但 RxSwift 提供了非常多的操作符, 我們將在後面接觸一點。
這裏是我想要實現的想法:
- 1、我將每個動畫包裹成一個函數,返回一個觀察者對象
Observable<Void>
- 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: 100, 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
操作符中的值
現在,我們進行更高級的封裝來實現動畫功能。將 動畫添加到 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: 100, 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)
over!!!