
Setahun setengah yang lalu, saya
menyanyikan pujian RxSwift . Butuh beberapa saat untuk mengetahuinya, tetapi ketika itu terjadi, tidak ada jalan untuk kembali. Sekarang saya memiliki palu terbaik di dunia, dan terkutuklah saya jika segala sesuatu di sekitar saya tidak tampak seperti paku.
Apple memperkenalkan kerangka kerja
Combine di
WWDC Summer Conference. Sekilas, sepertinya versi RxSwift sedikit lebih baik. Sebelum saya bisa menjelaskan apa yang saya sukai dan apa yang tidak, kita perlu memahami masalah apa yang Combine rancang untuk pecahkan.
Pemrograman reaktif? Jadi apa
Komunitas ReactiveX - di mana komunitas RxSwift adalah bagian - menjelaskan esensinya sebagai berikut:
API untuk pemrograman asinkron dengan utas yang dapat diamati.
Dan juga:
ReactiveX adalah kombinasi dari ide-ide terbaik dari pola desain Observer dan Iterator, serta pemrograman fungsional.
Baiklah ... oke.
Dan apa artinya ini?
Dasar-dasarnya
Untuk benar-benar memahami esensi pemrograman reaktif, saya merasa berguna untuk memahami bagaimana kita mendapatkannya. Pada artikel ini, saya akan menjelaskan bagaimana Anda dapat melihat jenis yang ada dalam bahasa OOP modern, memutar mereka, dan kemudian datang ke pemrograman reaktif.
Pada artikel ini, kita akan dengan cepat menyelidiki hutan, yang tidak
mutlak diperlukan untuk memahami pemrograman reaktif.
Namun, saya menganggap ini sebagai latihan akademis yang aneh, terutama dalam hal seberapa kuat bahasa yang diketik dapat membawa kita ke penemuan baru.
Jadi tunggu posting saya berikutnya jika Anda tertarik dengan detail baru.
Terhitung
"
Pemrograman reaktif " yang saya kenal berasal dari bahasa yang pernah saya tulis - C #. Premisnya sendiri cukup sederhana:
Bagaimana jika, alih-alih mengekstraksi nilai dari yang dapat dihitung, mereka akan mengirimkan sendiri nilai-nilai itu kepada Anda?Gagasan ini, "dorong bukannya tarikan," paling baik
dijelaskan oleh Brian Beckman dan Eric Meyer. 36 menit pertama ... Saya tidak mengerti apa-apa, tetapi
mulai dari sekitar menit ke-36 itu menjadi
sangat menarik.
Singkatnya, mari kita merumuskan kembali gagasan sekelompok objek linear dalam Swift, serta objek yang dapat beralih pada grup linier ini. Anda dapat melakukan ini dengan mendefinisikan protokol Swift palsu ini:
Ganda
Mari balikkan semuanya dan hasilkan
ganda . Kami akan mengirim data ke tempat asalnya. Dan dapatkan data dari tempat mereka pergi. Kedengarannya tidak masuk akal, tapi tahanlah sedikit.
Enumerable ganda
Mari kita mulai dengan Enumerable:
Karena
getEnumerator()
mengambil
Void
dan memberikan
Enumerator
, sekarang kami menerima
Enumerator
[dobel] dan memberikan
Void
.
Saya tahu ini aneh. Jangan pergi.
Pencacah Ganda
Lalu apa itu
DualOfEnumerator
?
Ada beberapa masalah di sini:
- Tidak ada konsep properti set-only di Swift.
- Apa yang terjadi dengan
throws
di Enumerator.moveNext()
?
- Apa yang terjadi dengan
Disposable
?
Untuk memperbaiki masalah dengan properti set-only, kita dapat memperlakukannya seperti apa adanya - sebuah fungsi. Mari kita
DualOfEnumerator
kita
DualOfEnumerator
:
protocol DualOfEnumerator {
Untuk memecahkan masalah dengan
throws
, mari kita pisahkan kesalahan yang mungkin terjadi di
moveNext()
dan bekerja dengannya sebagai fungsi
error()
terpisah
error()
:
protocol DualOfEnumerator {
Kita bisa melakukan sesuatu yang lain: lihat tanda tangan dari penyelesaian iterasi:
func enumeratorIsDone(Bool)
Mungkin sesuatu yang serupa akan terjadi seiring waktu:
enumeratorIsDone(false) enumeratorIsDone(false)
Sekarang, mari sederhanakan semuanya dan panggil
enumeratorIsDone
hanya ketika ... semuanya benar-benar siap. Dipandu oleh ide ini, kami menyederhanakan kode:
protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Jaga diri kita sendiri
Bagaimana dengan
Disposable
? Apa hubungannya dengan itu? Karena
Disposable
adalah bagian
dari tipe Enumerator
, ketika kita mendapatkan
Enumerator
double , itu mungkin seharusnya tidak ada di
Enumerator
. Sebaliknya, itu harus menjadi bagian dari
DualOfEnumerable
. Tapi dimana tepatnya?
DualOfEnumerator
sini:
func subscribe(DualOfEnumerator)
Jika kami menerima
DualOfEnumerator
,
DualOfEnumerator
seharusnya
Disposable
dikembalikan ?
Inilah jenis ganda yang Anda dapatkan:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Sebut saja bunga mawar, meski tidak
Jadi, sekali lagi, inilah yang kami dapat:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Mari kita bermain sedikit dengan namanya sekarang.
Mari kita mulai dengan
DualOfEnumerator
. Kami akan datang dengan nama yang lebih baik untuk fungsi untuk lebih akurat menggambarkan apa yang terjadi:
protocol DualOfEnumerator { func onComplete() func onError(Error) func onNext(Element) }
Jauh lebih baik dan lebih bisa dimengerti.
Bagaimana dengan mengetikkan nama? Mereka hanya mengerikan. Mari kita ubah sedikit.
DualOfEnumerator
- sesuatu yang mengikuti apa yang terjadi pada sekelompok objek linear. Kita dapat mengatakan bahwa dia mengamati kelompok linier.
DualOfEnumerable
adalah subjek observasi. Apa yang kami tonton. Karena itu, bisa disebut observable .
Sekarang buat perubahan terakhir dan dapatkan yang berikut:
protocol Observable { func subscribe(Observer) → Disposable } protocol Observer { func onComplete() func onError(Error) func onNext(Element) }
Wow
Kami baru saja membuat dua objek mendasar di RxSwift. Anda dapat melihat versi aslinya di
sini dan di
sini . Perhatikan bahwa dalam kasus Observer, fungsi tiga
on()
digabungkan menjadi satu
on(Event)
, di mana
Event
adalah enumerasi yang menentukan apa acara - penyelesaian, nilai berikutnya atau kesalahan.
Kedua jenis ini mendasari pemrograman RxSwift dan reaktif.
Tentang protokol palsu
Dua protokol "palsu" yang saya sebutkan di atas sebenarnya tidak palsu sama sekali. Ini adalah analog dari tipe yang ada di Swift:
Jadi apa
Jadi apa yang harus dikhawatirkan?
Begitu banyak dalam perkembangan modern -
terutama pengembangan aplikasi - dikaitkan dengan asinkron. Pengguna tiba-tiba mengklik tombol. Pengguna tiba-tiba memilih tab di UISegmentControl. Pengguna tiba-tiba memilih tab di UITabBar. Soket web tiba-tiba memberi kami informasi baru. Unduhan ini tiba-tiba - dan akhirnya - berakhir. Tugas latar belakang ini berakhir dengan tiba-tiba. Daftar ini terus berlanjut.
Di dunia CocoaTouch modern, ada banyak cara untuk menangani acara seperti ini:
- pemberitahuan
- panggilan balik
- Pengamatan Nilai-Kunci (KVO),
- target / mekanisme aksi.
Bayangkan jika
semuanya bisa direfleksikan dalam satu antarmuka tunggal. Yang bisa bekerja dengan
segala jenis data atau peristiwa asinkron dalam seluruh aplikasi.
Sekarang bayangkan jika akan ada seluruh
rangkaian fungsi yang memungkinkan Anda untuk memodifikasi
aliran ini, mengubahnya dari satu jenis ke yang lain, mengekstrak informasi dari Elemen, atau bahkan menggabungkannya dengan aliran lain.
Tiba-tiba, di tangan kita adalah seperangkat alat
universal baru.
Jadi, kami kembali ke awal:
API untuk pemrograman asinkron dengan utas yang dapat diamati.
Inilah yang membuat RxSwift alat yang ampuh. Seperti Combine.
Apa selanjutnya
Jika Anda ingin membaca lebih lanjut tentang RxSwift
dalam praktek , maka saya merekomendasikan
lima artikel saya yang ditulis pada tahun 2016 . Mereka menggambarkan pembuatan aplikasi CocoaTouch sederhana, diikuti oleh konversi bertahap ke RxSwift.
Dalam salah satu artikel berikut ini saya akan menjelaskan mengapa banyak teknik yang dijelaskan dalam
seri artikel saya
untuk pemula tidak berlaku di Combine, dan saya juga membandingkan Combine dengan RxSwift.
Combine: apa gunanya?
Diskusi Combine juga mencakup diskusi tentang perbedaan utama antara itu dan RxSwift. Bagi saya ada tiga dari mereka:
- kemungkinan menggunakan kelas non-reaktif,
- penanganan kesalahan
- tekanan balik.
Saya akan mencurahkan artikel terpisah untuk setiap item. Saya akan mulai dengan yang pertama.
Fitur dari RxCocoa
Dalam
posting sebelumnya, saya mengatakan bahwa RxSwift lebih dari ... RxSwift. Ini memberikan banyak kemungkinan untuk menggunakan kontrol dari UIKit dalam sub-jenis-tapi-tidak-cukup dari RxCocoa. Selain itu,
Komunitas RxSwift melangkah lebih jauh dan menerapkan banyak ikatan untuk jalan-jalan terpencil UIKit yang lebih terpencil, serta beberapa kelas CocoaTouch lain yang belum dicakup oleh RxSwift dan RxCocoa.
Oleh karena itu, sangat mudah untuk mendapatkan aliran yang Dapat
Observable
dari, katakanlah, mengklik UIButton. Saya akan memberikan contoh ini lagi:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Ringan
Mari (akhirnya) tetap membicarakan Combine
Combine sangat mirip dengan RxSwift. Seperti yang dikatakan dalam dokumentasi:
Kerangka kerja Combine menyediakan Swift API deklaratif untuk menangani nilai dari waktu ke waktu.
Kedengarannya familier: ingat deskripsi ReactiveX (proyek induk untuk RxSwift):
API untuk pemrograman asinkron dengan utas yang dapat diamati.
Dalam kedua kasus, hal yang sama dikatakan. Hanya saja istilah spesifik digunakan dalam deskripsi ReactiveX. Itu dapat dirumuskan kembali sebagai berikut:
API untuk pemrograman asinkron dengan nilai dari waktu ke waktu.
Hampir sama dengan saya.
Sama seperti sebelumnya
Ketika saya mulai menganalisis API, segera menjadi jelas bahwa sebagian besar tipe yang saya tahu dari RxSwift memiliki opsi serupa di Combine:
- Dapat diobservasi → Penerbit
- Pengamat → Pelanggan
- Sekali pakai → Dapat dibatalkan . Ini adalah kemenangan pemasaran. Anda tidak bisa membayangkan berapa banyak penampilan terkejut yang saya terima dari pengembang yang lebih tidak memihak ketika saya mulai menggambarkan Disposable di RxSwift.
- SchedulerType → Scheduler
Sejauh ini bagus. Sekali lagi, saya suka Cancellable lebih dari Disposable. Pengganti yang hebat, tidak hanya dalam hal pemasaran, tetapi juga dalam hal deskripsi yang akurat tentang esensi objek.
Lebih banyak bahkan lebih baik!
Ini tidak segera jelas, tetapi secara rohani mereka melayani satu tujuan, dan tidak satu pun dari mereka dapat menimbulkan kesalahan.
"Istirahat untuk kotoran"
Semuanya berubah segera setelah Anda mulai mempelajari RxCocoa. Ingat contoh di atas, di mana kami ingin mendapatkan aliran yang Dapat Diobservasi yang mewakili klik pada UIButton? Ini dia:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Combine membutuhkan ... lebih banyak pekerjaan untuk melakukan hal yang sama.
Combine tidak menyediakan kemampuan apa pun untuk mengikat ke objek UIKit.Ini ... hanya gelandangan yang tidak nyata.
Berikut adalah cara umum untuk mendapatkan
UIControl.Event dari
UIControl menggunakan Combine:
class ControlPublisher<T: UIControl>: Publisher { typealias ControlEvent = (control: UIControl, event: UIControl.Event) typealias Output = ControlEvent typealias Failure = Never let subject = PassthroughSubject<Output, Failure>() convenience init(control: UIControl, event: UIControl.Event) { self.init(control: control, events: [event]) } init(control: UIControl, events: [UIControl.Event]) { for event in events { control.addTarget(self, action: #selector(controlAction), for: event) } } @objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) { subject.send(ControlEvent(control: sender, event: event)) } func receive<S>(subscriber: S) where S : Subscriber, ControlPublisher.Failure == S.Failure, ControlPublisher.Output == S.Input { subject.receive(subscriber: subscriber) } }
Ini ... lebih
banyak pekerjaan. Setidaknya panggilannya terlihat seperti:
ControlPublisher(control: self.button, event: .touchUpInside) .sink { print("Tap!") }
Sebagai perbandingan, RxCocoa memberikan kakao yang enak dan menyenangkan dalam bentuk ikatan ke benda-benda UIKit:
self.button.rx.tap .subscribe(onNext: { _ in print("Tap!") })
Sendiri, tantangan-tantangan ini pada akhirnya benar-benar sangat mirip. Satu-satunya hal yang membuat frustrasi adalah saya harus menulis
ControlPublisher
sendiri untuk sampai ke titik ini. Selain itu, RxSwift dan RxCocoa telah diuji dengan sangat baik dan digunakan dalam proyek-proyek lebih dari tambang.
Sebagai perbandingan,
ControlPublisher
saya hanya muncul ... sekarang. Hanya karena jumlah klien (nol) dan waktu penggunaan di dunia nyata (hampir nol dibandingkan dengan RxCocoa) kode saya dianggap jauh lebih berbahaya.
Nyebelin.
Bantuan komunitas?
Sejujurnya, tidak ada yang menghalangi komunitas untuk membuat open source sendiri "CombineCocoa", yang akan mengisi celah RxCocoa seperti yang dilakukan oleh RxSwiftCommunity.
Namun, saya menganggap ini sebagai kelemahan besar Combine. Saya tidak ingin menulis ulang seluruh RxCocoa, hanya untuk mendapatkan binding ke objek UIKit.
Jika saya memutuskan untuk bertaruh pada
SwiftUI , maka saya kira ini akan
menghilangkan masalah kurangnya binding. Bahkan
aplikasi kecil saya berisi
banyak kode UI. Membuang semua ini hanya untuk melompat ke kereta Combine akan setidaknya bodoh, atau bahkan berbahaya.
Ngomong-ngomong, artikel di
Acara Penerima dan Penanganan dengan dokumentasi
Combine secara singkat menjelaskan cara menerima dan memproses acara di Combine. Pendahuluannya bagus, ini menunjukkan cara mengekstraksi nilai dari bidang teks dan menyimpannya dalam objek model kustom. Dokumentasi juga menunjukkan penggunaan operator untuk melakukan beberapa modifikasi lebih lanjut ke aliran yang dimaksud.
Contoh
Mari kita lanjutkan ke bagian akhir dokumentasi, di mana contoh kode adalah:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel)
Saya punya ... banyak masalah dengan ini.
Beri tahu Anda bahwa saya tidak menyukainya
Dua baris pertama menyebabkan saya paling banyak pertanyaan:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField)
NotificationCenter adalah sesuatu seperti bus aplikasi (atau bahkan bus sistem) di mana banyak orang dapat membuang data atau menangkap potongan informasi yang terbang. Solusi ini dari kategori all-and-for-all, sebagaimana dimaksud oleh pembuatnya. Dan memang ada banyak situasi di mana Anda mungkin perlu mencari tahu, katakanlah, keyboard baru saja ditampilkan atau disembunyikan. NotificationCenter adalah cara terbaik untuk mendistribusikan pesan ini ke seluruh sistem.
Tetapi bagi saya
NotificationCenter adalah
kode dengan choke . Ada saatnya (seperti mendapatkan notifikasi tentang keyboard) ketika NotificationCenter sebenarnya merupakan solusi
terbaik untuk masalah tersebut. Tetapi terlalu sering bagi saya NotificationCenter adalah solusi
paling nyaman . Benar-benar sangat nyaman untuk menjatuhkan sesuatu di NotificationCenter dan mengambilnya di tempat lain dalam aplikasi.
Selain itu, NotificationCenter adalah
"string" -typed , yaitu, Anda dapat dengan mudah membuat kesalahan dengan pemberitahuan yang Anda coba terbitkan atau dengarkan. Swift melakukan segala yang mungkin untuk memperbaiki situasi, tetapi NSString yang sama masih ada di bawah kap.
Tentang KVO
Di platform Apple, sudah ada cara populer untuk menerima pemberitahuan perubahan di berbagai bagian kode: observasi nilai kunci (KVO). Apple menggambarkannya seperti ini:
Ini adalah mekanisme yang memungkinkan objek menerima pemberitahuan perubahan pada properti yang ditentukan dari objek lain.
Berkat
tweet Gui Rambo, saya perhatikan bahwa Apple menambahkan binding KVO ke Combine. Ini bisa berarti bahwa saya bisa menghilangkan banyak kekecewaan tentang kurangnya analog RxCocoa di Combine. Jika saya dapat menggunakan KVO, ini mungkin akan menghilangkan kebutuhan untuk CombineCocoa, jadi untuk berbicara.
Saya mencoba mencari tahu contoh menggunakan KVO untuk mendapatkan nilai dari
UITextField
dan output ke konsol:
let sub = self.textField.publisher(for: \UITextField.text) .sink(receiveCompletion: { _ in print("Completed") }, receiveValue: { print("Text field is currently \"\($0)\"") })
Terlihat bagus, teruskan?
Tidak secepat itu, teman.
Saya lupa tentang
kebenaran yang tidak nyaman :
UIKit, pada umumnya, tidak kompatibel dengan KVO.
Dan tanpa dukungan KVO, ide saya tidak akan berfungsi. Cek saya mengkonfirmasi ini: kode tidak menampilkan apa pun ke konsol ketika saya memasukkan teks di bidang.
Jadi, harapan saya untuk menyingkirkan kebutuhan ikatan UIKit itu indah, tapi tidak lama.
Membersihkan
Masalah Combine yang lain adalah masih sepenuhnya tidak jelas di mana dan bagaimana membebaskan sumber daya dalam objek
Cancellable .
Tampaknya kita harus menyimpannya dalam variabel instan. Tapi saya tidak ingat bahwa dalam dokumentasi resmi ada sesuatu yang dikatakan tentang pembersihan.
RxSwift memiliki DisposeBag yang sangat bernama tapi sangat nyaman. Tidak mudah untuk membuat CancelBag di Combine, tetapi saya tidak yakin bahwa dalam hal ini ini adalah solusi terbaik.
Pada
artikel selanjutnya kita akan berbicara tentang penanganan kesalahan dalam RxSwift dan Combine, tentang kelebihan dan kekurangan dari kedua pendekatan.