RxSwift und Animationen in iOS
Veröffentlicht: 2022-03-11Wenn Sie ein iOS-Entwickler sind, der eine angemessene Menge an UI-Arbeiten geleistet hat und sich dafür begeistert, müssen Sie die Leistungsfähigkeit von UIKit lieben, wenn es um Animationen geht. Das Animieren einer UIView ist kinderleicht. Sie müssen nicht viel darüber nachdenken, wie Sie es im Laufe der Zeit verblassen, drehen, verschieben oder schrumpfen/erweitern lassen. Es wird jedoch etwas kompliziert, wenn Sie Animationen miteinander verketten und Abhängigkeiten zwischen ihnen einrichten möchten. Ihr Code kann am Ende ziemlich ausführlich und schwer zu folgen sein, mit vielen verschachtelten Schließungen und Einrückungsebenen.
In diesem Artikel werde ich untersuchen, wie man die Leistungsfähigkeit eines reaktiven Frameworks wie RxSwift nutzt, um diesen Code viel sauberer aussehen zu lassen und leichter zu lesen und zu befolgen. Die Idee kam mir, als ich an einem Projekt für einen Kunden arbeitete. Dieser spezielle Kunde war sehr UI-versiert (was perfekt zu meiner Leidenschaft passte)! Sie wollten, dass sich die Benutzeroberfläche ihrer App auf ganz besondere Weise verhält, mit einer ganzen Menge sehr eleganter Übergänge und Animationen. Eine ihrer Ideen war, ein Intro für die App zu haben, das die Geschichte erzählt, worum es in der App geht. Sie wollten, dass diese Geschichte durch eine Abfolge von Animationen erzählt wird und nicht durch das Abspielen eines vorgerenderten Videos, damit sie leicht optimiert und abgestimmt werden kann. RxSwift hat sich als perfekte Wahl für dieses Problem erwiesen, wie Sie hoffentlich nach Abschluss des Artikels feststellen werden.
Kurze Einführung in die reaktive Programmierung
Die reaktive Programmierung entwickelt sich zu einem Grundnahrungsmittel und wurde in den meisten modernen Programmiersprachen übernommen. Es gibt viele Bücher und Blogs, die ausführlich erklären, warum die reaktive Programmierung ein so leistungsstarkes Konzept ist und wie sie dabei hilft, gutes Softwaredesign zu fördern, indem sie bestimmte Designprinzipien und -muster durchsetzt. Es gibt Ihnen auch ein Toolkit, das Ihnen helfen kann, Code-Unordnung erheblich zu reduzieren.
Ich möchte einen Aspekt ansprechen, der mir wirklich gefällt – die Leichtigkeit, mit der Sie asynchrone Operationen verketten und auf eine deklarative, leicht lesbare Weise ausdrücken können.
Wenn es um Swift geht, gibt es zwei konkurrierende Frameworks, die Ihnen helfen, es in eine reaktive Programmiersprache zu verwandeln: ReactiveSwift und RxSwift. Ich werde RxSwift in meinen Beispielen nicht verwenden, weil es besser ist, sondern weil ich damit vertrauter bin. Ich gehe davon aus, dass Sie, der Leser, auch damit vertraut sind, damit ich direkt auf den Punkt kommen kann.
Verkettungsanimationen: Der alte Weg
Angenommen, Sie möchten eine Ansicht um 180° drehen und dann ausblenden. Sie könnten den completion nutzen und so etwas tun:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) Es ist etwas klobig, aber noch okay. Aber was ist, wenn Sie eine weitere Animation dazwischen einfügen möchten, z. B. die Ansicht nach rechts verschieben, nachdem sie sich gedreht hat und bevor sie ausgeblendet wird? Wenn Sie den gleichen Ansatz anwenden, erhalten Sie am Ende Folgendes:
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 }) }) }) Je mehr Schritte Sie hinzufügen, desto gestaffelter und umständlicher wird es. Und wenn Sie sich entscheiden, die Reihenfolge bestimmter Schritte zu ändern, müssen Sie eine nicht triviale Ausschneide- und Einfügesequenz ausführen, die fehleranfällig ist.
Nun, Apple hat offensichtlich daran gedacht – sie bieten eine bessere Möglichkeit, dies zu tun, indem sie eine Keyframe-basierte Animations-API verwenden. Mit diesem Ansatz könnte der obige Code wie folgt umgeschrieben werden:
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 }) })Das ist eine große Verbesserung, mit den wichtigsten Vorteilen:
- Der Code bleibt flach, unabhängig davon, wie viele Schritte Sie ihm hinzufügen
- Das Ändern der Reihenfolge ist unkompliziert (mit einer Einschränkung unten)
Der Nachteil dieses Ansatzes besteht darin, dass Sie in relativen Dauern denken müssen und es schwierig (oder zumindest nicht sehr einfach) wird, das absolute Timing oder die Reihenfolge der Schritte zu ändern. Denken Sie nur darüber nach, welche Berechnungen Sie durchführen müssten und welche Änderungen Sie an der Gesamtdauer und der relativen Dauer/Startzeit für jede der Animationen vornehmen müssten, wenn Sie sich entscheiden würden, die Ansicht innerhalb von 1 Sekunde statt einer halben Sekunde auszublenden zweitens, während alles andere gleich bleibt. Das Gleiche gilt, wenn Sie die Reihenfolge der Schritte ändern möchten – Sie müssten ihre relativen Startzeiten neu berechnen.
Angesichts der Nachteile finde ich keinen der oben genannten Ansätze gut genug. Die ideale Lösung, die ich suche, sollte folgende Kriterien erfüllen:
- Der Code muss unabhängig von der Anzahl der Schritte flach bleiben
- Ich sollte in der Lage sein, Animationen einfach hinzuzufügen/zu entfernen oder neu anzuordnen und ihre Dauer unabhängig voneinander ohne Nebenwirkungen auf andere Animationen zu ändern
Verkettungsanimationen: Der RxSwift-Weg
Ich habe festgestellt, dass ich mit RxSwift diese beiden Ziele leicht erreichen kann. RxSwift ist nicht das einzige Framework, das Sie verwenden könnten, um so etwas zu tun – jedes Promise-basierte Framework, mit dem Sie asynchrone Operationen in Methoden verpacken können, die syntaktisch miteinander verkettet werden können, ohne Verwendung von Vervollständigungsblöcken zu verwenden, reicht aus. Aber RxSwift hat mit seiner Reihe von Operatoren noch viel mehr zu bieten, worauf wir später noch eingehen werden.
Hier ist die Skizze, wie ich das machen werde:
- Ich werde jede der Animationen in eine Funktion packen, die eine Observable vom Typ
Observable<Void>zurückgibt. - Dieses Observable gibt nur ein Element aus, bevor es die Sequenz abschließt.
- Das Element wird ausgegeben, sobald die von der Funktion umschlossene Animation abgeschlossen ist.
- Ich werde diese Observables mit dem
flatMapOperator verketten.
So können meine Funktionen aussehen:

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() } }Und so füge ich das Ganze zusammen:
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)Es ist sicherlich viel mehr Code als in den vorherigen Implementierungen und mag für eine so einfache Sequenz von Animationen wie ein bisschen Overkill aussehen, aber das Schöne ist, dass es erweitert werden kann, um einige ziemlich komplexe Animationssequenzen zu handhaben, und aufgrund dessen sehr einfach zu lesen ist die deklarative Natur der Syntax.
Sobald Sie es in den Griff bekommen haben, können Sie Animationen erstellen, die so komplex wie ein Film sind, und haben eine große Auswahl an praktischen RxSwift-Operatoren zur Verfügung, die Sie anwenden können, um Dinge zu erreichen, die mit einem der oben genannten Ansätze sehr schwierig wären.
So können wir den .concat Operator verwenden, um meinen Code noch prägnanter zu machen – den Teil, in dem Animationen miteinander verkettet werden:
Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)Sie können Verzögerungen zwischen Animationen wie folgt einfügen:
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)Nehmen wir nun an, wir möchten, dass sich die Ansicht eine bestimmte Anzahl von Malen dreht, bevor sie sich zu bewegen beginnt. Und wir möchten einfach anpassen, wie oft es sich drehen soll.
Zuerst erstelle ich eine Methode, die die Rotationsanimation kontinuierlich wiederholt und nach jeder Rotation ein Element ausgibt. Ich möchte, dass diese Drehungen aufhören, sobald das Observable beseitigt ist. Ich könnte so etwas machen:
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 } } }Und dann könnte meine schöne Animationskette so aussehen:
Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag) Sie sehen, wie einfach es ist, zu steuern, wie oft die Ansicht gedreht wird – ändern Sie einfach den an den take -Operator übergebenen Wert.
Nun möchte ich mit meiner Implementierung einen Schritt weiter gehen, indem ich jede der Animationsfunktionen, die ich erstellt habe, in die „Reactive“-Erweiterung von UIView (zugänglich über das .rx Suffix) umschließt. Dies würde es eher in Richtung der RxSwift-Konventionen bringen, bei denen auf reaktive Funktionen normalerweise über das Suffix .rx zugegriffen wird, um deutlich zu machen, dass sie eine Observable zurückgeben.
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 } } } }Damit kann ich sie so zusammensetzen:
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)Wohin von hier aus
Wie dieser Artikel zeigt, können Sie durch das Entfesseln der Kraft von RxSwift und sobald Sie Ihre Grundelemente eingerichtet haben, wirklich Spaß mit Animationen haben. Ihr Code ist sauber und leicht lesbar und sieht nicht mehr wie „Code“ aus – Sie „beschreiben“, wie Ihre Animationen zusammengesetzt sind, und sie werden einfach lebendig! Wenn Sie etwas Ausgefeilteres als das hier beschriebene tun möchten, können Sie dies sicherlich tun, indem Sie weitere eigene Primitive hinzufügen, die andere Arten von Animationen einkapseln. Sie können auch jederzeit das ständig wachsende Arsenal an Tools und Frameworks nutzen, die von einigen leidenschaftlichen Menschen in der Open-Source-Community entwickelt wurden.
Wenn Sie ein RxSwift-Konvertierter sind, besuchen Sie regelmäßig das RxSwiftCommunity-Repository: Sie können immer etwas Neues finden!
Wenn Sie nach weiteren UI-Tipps suchen, lesen Sie How to Implement a Pixel-perfect iOS UI Design von Toptaler Roman Stetsenko.
Source Notes (Grundlagen verstehen)
- Wikipedia - Swift (Programmiersprache)
- Lynda.com - RxSwift: Designmuster für iOS-Entwickler
- We Heart Swift – UIView-Grundlagen
- Eckig - Observables
