RxSwift și animații în iOS
Publicat: 2022-03-11Dacă sunteți un dezvoltator iOS care a făcut o cantitate rezonabilă de lucru cu UI și este pasionat de asta, trebuie să vă placă puterea UIKit atunci când vine vorba de animații. Animarea unui UIView este la fel de ușor ca un tort. Nu trebuie să te gândești prea mult la cum să-l faci să se estompeze, să se rotească, să se miște sau să se micșoreze/extinde în timp. Cu toate acestea, se implică puțin dacă doriți să înlănțuiți animațiile și să configurați dependențe între ele. Codul dvs. poate ajunge să fie destul de pronunțat și greu de urmărit, cu multe închideri imbricate și niveluri de indentare.
În acest articol, voi explora cum să aplic puterea unui cadru reactiv, cum ar fi RxSwift, pentru a face codul să pară mult mai curat, precum și mai ușor de citit și urmat. Ideea mi-a venit când lucram la un proiect pentru un client. Clientul respectiv era foarte priceput la UI (care se potrivea perfect cu pasiunea mea)! Au vrut ca interfața de utilizare a aplicației lor să se comporte într-un mod foarte special, cu o mulțime de tranziții și animații foarte elegante. Una dintre ideile lor a fost să aibă o introducere în aplicație, care să spună povestea despre ce este vorba în aplicație. Ei au vrut ca povestea să fie spusă printr-o secvență de animații, mai degrabă decât prin redarea unui videoclip pre-redat, astfel încât să poată fi ajustat și reglat cu ușurință. RxSwift s-a dovedit a fi o alegere perfectă pentru o astfel de problemă, așa cum sper să vă dați seama odată ce veți termina articolul.
Scurtă introducere în programarea reactivă
Programarea reactivă devine un element de bază și a fost adoptată în majoritatea limbajelor de programare moderne. Există o mulțime de cărți și bloguri care explică în detaliu de ce programarea reactivă este un concept atât de puternic și cum ajută la încurajarea unui design bun de software prin aplicarea anumitor principii și modele de design. De asemenea, vă oferă un set de instrumente care vă poate ajuta să reduceți în mod semnificativ aglomerația de coduri.
Aș dori să ating un aspect care îmi place foarte mult – ușurința cu care puteți înlănțui operațiuni asincrone și le puteți exprima într-un mod declarativ, ușor de citit.
Când vine vorba de Swift, există două cadre concurente care vă ajută să îl transformați într-un limbaj de programare reactiv: ReactiveSwift și RxSwift. Voi folosi RxSwift în exemplele mele nu pentru că este mai bun, ci pentru că sunt mai familiar cu el. Voi presupune că și dumneavoastră, cititorul, sunteți familiarizat cu el, astfel încât să pot ajunge direct la carnea lui.
Înlănțuirea animațiilor: Vechea cale
Să presupunem că doriți să rotiți o vizualizare cu 180° și apoi să o eliminați. Puteți folosi închiderea completion și faceți ceva de genul acesta:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) Este puțin voluminos, dar totuși în regulă. Dar dacă doriți să inserați încă o animație între ele, să spunem să mutați vizualizarea la dreapta după ce s-a rotit și înainte ca aceasta să dispară? Aplicând aceeași abordare, veți ajunge la ceva de genul acesta:
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 }) }) }) Cu cât adăugați mai mulți pași, cu atât devine mai eșalonat și mai greoi. Și apoi, dacă decideți să schimbați ordinea anumitor pași, va trebui să efectuați o secvență non-trivială de tăiere și lipire, care este predispusă la erori.
Ei bine, Apple s-a gândit evident la asta – oferă o modalitate mai bună de a face acest lucru, folosind un API de animații bazat pe cadre cheie. Cu această abordare, codul de mai sus ar putea fi rescris astfel:
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 }) })Aceasta este o îmbunătățire majoră, cu avantajele cheie fiind:
- Codul rămâne plat, indiferent de câți pași adăugați la el
- Modificarea ordinii este simplă (cu o avertizare mai jos)
Dezavantajul acestei abordări este că trebuie să te gândești în termeni de durate relative și devine dificil (sau cel puțin nu foarte simplu) să schimbi momentul sau ordinea absolută a pașilor. Gândiți-vă doar la ce calcule ar trebui să treceți și la ce fel de modificări ar trebui să faceți duratei generale și duratelor/orelor de pornire relative pentru fiecare animație dacă ați decide să faceți ca vizualizarea să dispară în decurs de 1 secundă în loc de o jumătate de secundă. în al doilea rând, păstrând totul la fel. Același lucru este valabil și dacă doriți să schimbați ordinea pașilor - ar trebui să recalculați orele de început relative.
Având în vedere dezavantajele, nici una dintre abordările de mai sus nu mi se pare suficient de bună. Soluția ideală pe care o caut ar trebui să îndeplinească următoarele criterii:
- Codul trebuie să rămână plat, indiferent de numărul de pași
- Ar trebui să pot adăuga/elimina sau reordona cu ușurință animații și să le modific durata independent, fără efecte secundare pentru alte animații
Înlănțuirea animațiilor: Calea RxSwift
Am descoperit că, folosind RxSwift, pot realiza cu ușurință ambele obiective. RxSwift nu este singurul cadru pe care l-ați putea folosi pentru a face așa ceva – orice cadru bazat pe promisiuni care vă permite să încadrați operațiunile asincrone în metode care pot fi înlănțuite sintactic, fără a utiliza blocuri de completare. Dar RxSwift are mult mai multe de oferit cu gama sa de operatori, despre care vom aborda puțin mai târziu.
Iată schema cum voi face asta:
- Voi împacheta fiecare dintre animații într-o funcție care returnează un observabil de tip
Observable<Void>. - Acel observabil va emite doar un element înainte de a finaliza secvența.
- Elementul va fi emis de îndată ce animația înfășurată de funcție se finalizează.
- Voi înlănțui aceste observabile împreună folosind operatorul
flatMap.
Iată cum pot arăta funcțiile mele:
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() } }Și iată cum le-am adunat pe toate:

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)Este cu siguranță mult mai mult cod decât în implementările anterioare și poate părea un pic exagerat pentru o secvență atât de simplă de animații, dar frumusețea este că poate fi extins pentru a gestiona unele secvențe de animație destul de complexe și este foarte ușor de citit datorită natura declarativă a sintaxei.
Odată ce vă pricepeți, puteți crea animații la fel de complexe precum un film și aveți la dispoziție o mare varietate de operatori RxSwift la îndemână pe care îi puteți aplica pentru a realiza lucruri care ar fi foarte dificil de realizat cu oricare dintre abordările menționate mai sus.
Iată cum putem folosi operatorul .concat pentru a face codul meu și mai concis - partea în care animațiile sunt legate între ele:
Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)Puteți insera întârzieri între animații, astfel:
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)Acum, să presupunem că vrem ca vizualizarea să se rotească de un anumit număr de ori înainte de a începe să se miște. Și vrem să modificăm cu ușurință de câte ori ar trebui să se rotească.
Mai întâi voi face o metodă care repetă animația de rotație în mod continuu și emite un element după fiecare rotație. Vreau ca aceste rotații să se oprească de îndată ce observabilul este eliminat. As putea face asa ceva:
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 } } }Și apoi frumosul meu lanț de animații ar putea arăta astfel:
Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag) Vedeți cât de ușor este să controlați de câte ori se va roti vizualizarea - doar schimbați valoarea transmisă operatorului de take .
Acum, aș dori să duc implementarea mea cu un pas mai departe, împachetând fiecare funcții de animație pe care le-am creat în extensia „Reactivă” a UIView (accesabilă prin sufixul .rx ). Acest lucru l-ar face mai mult pe linia convențiilor RxSwift, unde funcțiile reactive sunt de obicei accesate prin sufixul .rx pentru a clarifica faptul că returnează un observabil.
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 } } } }Cu asta, le pot aduna astfel:
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)Unde să mergi de aici
După cum ilustrează acest articol, dezlănțuind puterea RxSwift și, odată ce ai primitivele la locul lor, te poți distra cu adevărat cu animațiile. Codul dvs. este curat și ușor de citit și nu mai arată ca „cod” – „descrieți” cum sunt asamblate animațiile dvs. și pur și simplu prind viață! Dacă doriți să faceți ceva mai elaborat decât este descris aici, cu siguranță puteți face asta adăugând mai multe primitive proprii, încapsulând alte tipuri de animații. De asemenea, puteți profita întotdeauna de arsenalul în continuă creștere de instrumente și cadre dezvoltate de niște oameni pasionați din comunitatea open source.
Dacă sunteți convertit la RxSwift, continuați să vizitați în mod regulat depozitul RxSwiftCommunity: puteți găsi întotdeauna ceva nou!
Dacă sunteți în căutarea mai multor sfaturi privind interfața de utilizare, încercați să citiți Cum să implementați un design de interfață de utilizare iOS perfect pentru pixeli de la colegul Toptaler Roman Stetsenko.
Note sursă (înțelegerea elementelor de bază)
- Wikipedia - Swift (limbaj de programare)
- Lynda.com - RxSwift: modele de design pentru dezvoltatorii iOS
- We Heart Swift - Noțiuni fundamentale UIView
- Angular - Observabile
