RxSwift dan Animasi di iOS
Diterbitkan: 2022-03-11Jika Anda seorang pengembang iOS yang telah melakukan pekerjaan UI dalam jumlah yang wajar dan sangat menyukainya, Anda harus menyukai kekuatan UIKit dalam hal animasi. Menganimasikan UIView semudah kue. Anda tidak perlu berpikir banyak tentang cara membuatnya memudar, memutar, bergerak, atau mengecil/memperluas seiring waktu. Namun, itu akan sedikit terlibat jika Anda ingin menyatukan animasi dan mengatur dependensi di antara mereka. Kode Anda mungkin akan menjadi sangat bertele-tele dan sulit diikuti, dengan banyak penutupan bersarang dan tingkat lekukan.
Dalam artikel ini, saya akan mengeksplorasi cara menerapkan kekuatan kerangka kerja reaktif seperti RxSwift untuk membuat kode tersebut terlihat lebih bersih serta lebih mudah dibaca dan diikuti. Idenya datang kepada saya ketika saya sedang mengerjakan sebuah proyek untuk klien. Klien tersebut sangat memahami UI (yang sangat cocok dengan hasrat saya)! Mereka ingin UI aplikasi mereka berperilaku dengan cara yang sangat khusus, dengan banyak transisi dan animasi yang sangat ramping. Salah satu ide mereka adalah memiliki intro ke aplikasi yang akan menceritakan kisah tentang aplikasi itu. Mereka ingin kisah itu diceritakan melalui serangkaian animasi daripada dengan memutar video yang telah dirender sebelumnya sehingga dapat dengan mudah diubah dan disetel. RxSwift ternyata menjadi pilihan yang sempurna untuk masalah seperti itu, karena saya harap Anda akan menyadarinya setelah Anda menyelesaikan artikel ini.
Pengantar Singkat untuk Pemrograman Reaktif
Pemrograman reaktif menjadi pokok dan telah diadopsi di sebagian besar bahasa pemrograman modern. Ada banyak buku dan blog di luar sana yang menjelaskan dengan sangat rinci mengapa pemrograman reaktif adalah konsep yang sangat kuat dan bagaimana hal itu membantu mendorong desain perangkat lunak yang baik dengan menerapkan prinsip dan pola desain tertentu. Ini juga memberi Anda toolkit yang dapat membantu Anda mengurangi kekacauan kode secara signifikan.
Saya ingin menyentuh satu aspek yang sangat saya sukai—kemudahan yang dapat digunakan untuk menyambungkan operasi asinkron dan mengekspresikannya dengan cara yang deklaratif dan mudah dibaca.
Ketika berbicara tentang Swift, ada dua kerangka kerja bersaing yang membantu Anda mengubahnya menjadi bahasa pemrograman reaktif: ReactiveSwift dan RxSwift. Saya akan menggunakan RxSwift dalam contoh saya bukan karena lebih baik tetapi karena saya lebih akrab dengannya. Saya akan berasumsi bahwa Anda, pembaca, sudah akrab dengannya sehingga saya bisa langsung memahami dagingnya.
Animasi Chaining: Cara Lama
Katakanlah Anda ingin memutar tampilan 180° dan kemudian memudarkannya. Anda dapat menggunakan penutupan completion dan melakukan sesuatu seperti ini:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) Agak kegedean sih, tapi masih oke. Tetapi bagaimana jika Anda ingin menyisipkan satu animasi lagi di antaranya, misalnya menggeser tampilan ke kanan setelah diputar dan sebelum menghilang? Menerapkan pendekatan yang sama, Anda akan berakhir dengan sesuatu seperti ini:
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 }) }) }) Semakin banyak langkah yang Anda tambahkan, semakin terhuyung-huyung dan rumit. Dan kemudian jika Anda memutuskan untuk mengubah urutan langkah-langkah tertentu, Anda harus melakukan beberapa urutan potong dan tempel non-sepele, yang rawan kesalahan.
Yah, Apple jelas telah memikirkannya—mereka menawarkan cara yang lebih baik untuk melakukan ini, menggunakan API animasi berbasis keyframe. Dengan pendekatan itu, kode di atas dapat ditulis ulang seperti ini:
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 }) })Itu peningkatan besar, dengan keuntungan utama adalah:
- Kode tetap datar terlepas dari berapa banyak langkah yang Anda tambahkan ke dalamnya
- Mengubah urutan itu mudah (dengan satu peringatan di bawah)
Kerugian dari pendekatan ini adalah Anda harus berpikir dalam jangka waktu relatif dan menjadi sulit (atau setidaknya tidak terlalu mudah) untuk mengubah waktu absolut atau urutan langkah-langkahnya. Pikirkan saja tentang perhitungan apa yang harus Anda lalui dan jenis perubahan apa yang harus Anda buat untuk durasi keseluruhan dan durasi relatif/waktu mulai untuk setiap animasi jika Anda memutuskan untuk membuat tampilan memudar dalam 1 detik, bukan setengah kedua sambil menjaga semuanya tetap sama. Hal yang sama berlaku jika Anda ingin mengubah urutan langkah—Anda harus menghitung ulang waktu mulai relatifnya.
Mengingat kerugiannya, saya tidak menemukan salah satu dari pendekatan di atas cukup baik. Solusi ideal yang saya cari harus memenuhi kriteria berikut:
- Kode harus tetap datar terlepas dari jumlah langkah
- Saya harus dapat dengan mudah menambah/menghapus atau menyusun ulang animasi dan mengubah durasinya secara mandiri tanpa efek samping ke animasi lain
Chaining Animations: The RxSwift Way
Saya menemukan bahwa dengan menggunakan, RxSwift, saya dapat dengan mudah mencapai kedua tujuan ini. RxSwift bukan satu-satunya kerangka kerja yang dapat Anda gunakan untuk melakukan sesuatu seperti itu—kerangka kerja berbasis janji apa pun yang memungkinkan Anda membungkus operasi asinkron ke dalam metode yang dapat dirantai secara sintaksis tanpa menggunakan blok penyelesaian akan berhasil. Tetapi RxSwift memiliki lebih banyak hal untuk ditawarkan dengan berbagai operatornya, yang akan kita bahas nanti.
Berikut adalah garis besar bagaimana saya akan melakukannya:
- Saya akan membungkus setiap animasi menjadi sebuah fungsi yang mengembalikan sebuah observable dari tipe
Observable<Void>. - Observable itu akan memancarkan hanya satu elemen sebelum menyelesaikan urutannya.
- Elemen akan dipancarkan segera setelah animasi yang dibungkus oleh fungsi selesai.
- Saya akan menghubungkan observable ini bersama-sama menggunakan operator
flatMap.
Ini adalah bagaimana fungsi saya dapat terlihat seperti:

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() } }Dan inilah cara saya menggabungkan semuanya:
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)Ini tentu saja lebih banyak kode daripada dalam implementasi sebelumnya dan mungkin terlihat seperti sedikit berlebihan untuk urutan animasi yang begitu sederhana, tetapi keindahannya adalah dapat diperluas untuk menangani beberapa urutan animasi yang cukup rumit dan sangat mudah dibaca karena sifat deklaratif dari sintaks.
Setelah Anda menguasainya, Anda dapat membuat animasi serumit film dan memiliki berbagai macam operator RxSwift praktis yang dapat Anda terapkan untuk mencapai hal-hal yang akan sangat sulit dilakukan dengan salah satu pendekatan yang disebutkan di atas.
Berikut adalah bagaimana kita dapat menggunakan operator .concat untuk membuat kode saya lebih ringkas—bagian di mana animasi dirangkai menjadi satu:
Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)Anda dapat memasukkan penundaan di antara animasi seperti ini:
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)Sekarang, mari kita asumsikan kita ingin tampilan berputar beberapa kali sebelum mulai bergerak. Dan kami ingin dengan mudah men-tweak berapa kali itu harus berputar.
Pertama saya akan membuat metode yang mengulangi animasi rotasi terus menerus dan memancarkan elemen setelah setiap rotasi. Saya ingin rotasi ini berhenti segera setelah yang dapat diamati dibuang. Saya bisa melakukan sesuatu seperti ini:
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 } } }Dan kemudian rangkaian animasi saya yang indah akan terlihat seperti ini:
Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag) Anda melihat betapa mudahnya mengontrol berapa kali tampilan akan diputar—cukup ubah nilai yang diteruskan ke operator take .
Sekarang, saya ingin mengambil implementasi saya satu langkah lebih jauh dengan membungkus setiap fungsi animasi yang saya buat ke dalam ekstensi "Reaktif" UIView (dapat diakses melalui akhiran .rx ). Ini akan membuatnya lebih sejalan dengan konvensi RxSwift, di mana fungsi reaktif biasanya diakses melalui akhiran .rx untuk memperjelas bahwa mereka mengembalikan yang dapat diamati.
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 } } } }Dengan itu, saya dapat menggabungkannya seperti ini:
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)Ke mana harus pergi dari sini?
Seperti yang diilustrasikan artikel ini, dengan melepaskan kekuatan RxSwift, dan begitu Anda memiliki primitif, Anda dapat bersenang-senang dengan animasi. Kode Anda bersih dan mudah dibaca, dan tidak terlihat seperti "kode" lagi—Anda "mendeskripsikan" bagaimana animasi Anda disatukan dan menjadi hidup! Jika Anda ingin melakukan sesuatu yang lebih rumit daripada yang dijelaskan di sini, Anda pasti dapat melakukannya dengan menambahkan lebih banyak primitif dari enkapsulasi jenis animasi lain Anda sendiri. Anda juga selalu dapat memanfaatkan gudang alat dan kerangka kerja yang terus berkembang yang dikembangkan oleh beberapa orang yang bersemangat dalam komunitas sumber terbuka.
Jika Anda adalah orang yang berkonversi RxSwift, terus kunjungi repositori RxSwiftCommunity secara teratur: Anda selalu dapat menemukan sesuatu yang baru!
Jika Anda mencari saran UI lainnya, coba baca Cara Menerapkan Desain UI iOS Pixel-perfect oleh sesama Toptaler Roman Stetsenko.
Catatan Sumber (Memahami Dasar-dasarnya)
- Wikipedia - Swift (bahasa pemrograman)
- Lynda.com - RxSwift: Pola Desain untuk Pengembang iOS
- We Heart Swift - Dasar-dasar UIView
- Sudut - Dapat Diamati
