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