iOSのRxSwiftとアニメーション
公開: 2022-03-11ある程度のUI作業を行い、それに情熱を注いでいるiOS開発者であれば、アニメーションに関してはUIKitのパワーを気に入るはずです。 UIViewのアニメーションはケーキのように簡単です。 時間の経過とともにフェード、回転、移動、または縮小/拡張する方法についてあまり考える必要はありません。 ただし、アニメーションをチェーンしてそれらの間に依存関係を設定する場合は、少し複雑になります。 多くのネストされたクロージャとインデントレベルがあるため、コードは非常に冗長でわかりにくいものになる可能性があります。
この記事では、RxSwiftなどのリアクティブフレームワークの機能を適用して、コードをよりクリーンに見せ、読みやすく、わかりやすくする方法について説明します。 私がクライアントのためのプロジェクトに取り組んでいたときに、そのアイデアが思い浮かびました。 その特定のクライアントは非常にUIに精通していました(これは私の情熱と完全に一致していました)! 彼らは、アプリのUIが非常に特殊な方法で動作し、非常に洗練されたトランジションとアニメーションがたくさんあることを望んでいました。 彼らのアイデアの1つは、アプリの概要を説明するアプリの紹介をすることでした。 彼らは、事前にレンダリングされたビデオを再生するのではなく、一連のアニメーションを通じてそのストーリーを伝え、簡単に調整および調整できるようにしたいと考えていました。 RxSwiftは、このような問題に最適な選択肢であることがわかりました。記事を読み終えたら、気付くようになることを願っています。
リアクティブプログラミングの簡単な紹介
リアクティブプログラミングは定番になりつつあり、現代のプログラミング言語のほとんどで採用されています。 リアクティブプログラミングが非常に強力な概念である理由と、特定の設計原則とパターンを適用することで優れたソフトウェア設計を促進するのにどのように役立つかを詳細に説明している本やブログがたくさんあります。 また、コードの乱雑さを大幅に減らすのに役立つツールキットも提供します。
私が本当に気に入っている側面の1つに触れたいと思います。それは、非同期操作を連鎖させ、宣言的で読みやすい方法で表現することができる容易さです。
Swiftに関しては、リアクティブプログラミング言語に変換するのに役立つ2つの競合するフレームワークがあります。ReactiveSwiftとRxSwiftです。 RxSwiftを例で使用するのは、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 } })
少しかさばりますが、それでも大丈夫です。 しかし、間にもう1つのアニメーションを挿入したい場合、たとえば、回転した後、フェードアウトする前にビューを右にシフトするとどうなりますか? 同じアプローチを適用すると、次のようになります。
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 }) }) })
それに追加するステップが多いほど、それはよりずらされて面倒になります。 そして、特定のステップの順序を変更することにした場合は、エラーが発生しやすい、重要なカットアンドペーストシーケンスを実行する必要があります。
そうですね、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つの注意点があります)
このアプローチの欠点は、相対的な期間の観点から考える必要があり、ステップの絶対的なタイミングまたは順序を変更することが困難になる(または少なくともそれほど単純ではない)ことです。 ビューを半分ではなく1秒以内にフェードさせることにした場合、どのような計算を実行する必要があり、各アニメーションの全体的な継続時間と相対的な継続時間/開始時間にどのような変更を加える必要があるかを考えてください。第二に、他のすべてを同じに保ちながら。 ステップの順序を変更する場合も同様です。相対的な開始時間を再計算する必要があります。
不利な点を考えると、上記のアプローチのどれも十分に良いとは思いません。 私が探している理想的なソリューションは、次の基準を満たす必要があります。
- ステップ数に関係なく、コードはフラットのままである必要があります
- アニメーションを簡単に追加/削除または並べ替えたり、他のアニメーションに副作用を与えることなく、アニメーションの長さを個別に変更したりできるはずです。
アニメーションの連鎖:RxSwift Way
RxSwiftを使用すると、これらの両方の目標を簡単に達成できることがわかりました。 RxSwiftは、そのようなことを行うために使用できる唯一のフレームワークではありません。完了ブロックを使用せずに構文的にチェーンできるメソッドに非同期操作をラップできる、promiseベースのフレームワークであればどれでもかまいません。 しかし、RxSwiftには、演算子の配列で提供できるものがたくさんあります。これについては、後で触れます。
これが私がそれを行う方法の概要です:
- 各アニメーションを、
Observable<Void>
型のobservableを返す関数にラップします。 - そのオブザーバブルは、シーケンスを完了する前に1つの要素のみを放出します。
- 関数によってラップされたアニメーションが完了するとすぐに、要素が放出されます。
-
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
演算子に渡される値を変更するだけです。
ここで、作成したアニメーション関数を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に関するその他のアドバイスをお探しの場合は、ToptalerRomanStetsenkoによるピクセルパーフェクトなiOSUIデザインの実装方法をお読みください。
ソースノート(基本を理解する)
- ウィキペディア-Swift(プログラミング言語)
- Lynda.com-RxSwift:iOS開発者向けのデザインパターン
- We Heart Swift-UIView Fundamentals
- Angular-Observables