Haloo.. Pada artikel kali ini kita akan berkenalan dengan Combine, mengapa kita membutuhkan Combine, serta bagaimana cara menggunakannya.
Combine merupakan sebuah framework yang diperkenalkan oleh Apple pada event WWDC tahun 2019, framework ini mempunyai syntax yang Declarative & ringkas, dan bertujuan untuk membuat program yang Reactive. artinya, jika ada data yang berubah maka data lain yang mempunyai keterkaitan dengan data tersebut pun akan ikut berubah juga.
Untuk memahaminya, perhatikan code berikut :
Imperative Program1let price = 50002var balance = 034let canBuy = balance >= price56// canBuy -> false78balance = 5000910// canBuy -> tetap false 🤔
Code di atas merupakan salah satu permasalahan jika kita menulis program secara Imperative. Nilai dari variabel canBuy tetap false meskipun kita sudah set variabel balance menjadi 5000.
Nah, Combine hadir untuk mengatasi permasalahan tersebut :
Reactive Program1let price = CurrentValueSubject<Int, Never>(5000)2let balance = CurrentValueSubject<Int, Never>(0)34let canBuy = balance5 .combineLatest(price)6 .map { $0 >= $1 }78// canBuy -> false910balance.send(5000)1112// canBuy -> true 🥳
Untuk sekarang, tidak perlu khawatir jika kamu kurang familiar dengan code di atas, kita akan mempelajarinya sebentar lagi. 😉
Meskipun Combine masih relatif baru dibandingkan Reactive Framework lainnya seperti RxSwift dan hanya support untuk iOS 13 ke atas, tapi menurut saya Combine ini masa depannya lebih cerah, karena dimaintain & mendapat dukungan dari Apple.
Combine juga telah digunakan oleh Apple pada Foundation Framework bahkan SwiftUI. dapat kita lihat pada URLSession.dataTaskPublisher, NotificationCenter, Property Wrapper: @Published, @ObservableObject, @EnvirontmentObject, dan masih banyak lagi.
Pada dasarnya, cara kerja dari Combine itu cukup sederhana, yaitu Publisher mengirimkan data kepada Subscriber. dan di tengah-tengah pengiriman data tersebut kita dapat memanipulasi datanya menggunakan yang namanya Operator. Koneksi antara Subscriber dengan Publisher dinamakan dengan Subscription. Kalau kita buat diagramnya, kira-kira seperti ini :
Publisher adalah sebuah Protocol yang menyatakan bahwa ia dapat mengirimkan data dari waktu ke waktu (tidak hanya sekali) kepada Subscriber. Subscriber ini bisa lebih dari satu.
Berbicara mengenai Publisher, ada satu konsep lagi yaitu Subject. Subject ini mirip seperti Publisher yaitu berfungsi untuk mengirim data kepada Subscriber. bedanya itu kalau Subject bersifat mutable dan kita bisa mengirim datanya secara manual menggunakan method send().
Ada banyak cara untuk membuat Publisher, salah satunya adalah menggunakan Just, Publisher Just ini berfungsi untuk mengirimkan sebuah data kepada masing-masing Subscriber hanya sekali saja kemudian selesai. Publisher Just juga hanya memegang satu tipe data saja yaitu Output, jadi dia tidak bisa gagal dengan cara mengirimkan Error seperti Publisher CurrentValueSubject pada contoh di atas.
Publisher Just cocok kita gunakan untuk mengirimkan data yang konstan. Berikut adalah contoh penggunaan dari Publisher Just :
Publisher Just1let publisher = Just(3)23publisher.sink(receiveCompletion: {4 print("Completed : \($0)")5}, receiveValue: {6 print("Value : \($0)")7})
Output1Value : 32Completed : finished
Selain Publisher Just, Kamu bisa melihat Publisher lainnya yang disediakan oleh Combine di sini : https://developer.apple.com/documentation/combine/publisher pada bagian Relationships -> Conforming Types.
Ada 2 jenis Subject, yaitu PassthroughSubject dan CurrentValueSubject :
PassthroughSubject : Berfungsi untuk merepresentasikan sebuah event.
PassthroughSubject1let onTappedButton = PassthroughSubject<Void, Never>()23onTappedButton.sink {4 print("Button Tapped!")5}67onTappedButton.send()
Output1Button Tapped!
CurrentValueSubject : Berfungsi untuk merepresentasikan sebuah state.
CurrentValueSubject1let balance = CurrentValueSubject<Int, Never>(0)23balance.sink { print("Current Balance: \($0)") }45balance.send(5000)
Output1Current Balance: 02Current Balance: 5000
Subscriber adalah sebuah Protocol yang menyatakan bahwa ia dapat menerima data yang berasal dari suatu Publisher.
Subscriber memegang 2 tipe data, yaitu Input dan Error. tipe data Input pada Subscriber harus sama dengan tipe data Output pada Pubslisher, begitu pula dengan tipe data Error, keduanya harus memiliki tipe data yang sama.
1Publisher<Output, Error>2Subscriber<Input, Error>
Koneksi antara Subscriber dengan Publisher dinamakan dengan Subscription. Ada 2 cara untuk membuat Subscription :
1. Menggunakan Operator sink(receiveCompletion:receiveValue:)
Pada beberapa contoh di atas, sebenarnya kita telah menggunakan Operator sink untuk membuat Subscription. Operator sink akan menjalankan closure yang kita berikan saat menerima sinyal Completion dan saat menerima data baru.
Operator sink1let publisher = Just(3)24publisher.sink(receiveCompletion: {5 print("Completed : \($0)")6}, receiveValue: {7 print("Value : \($0)")8})
Output1Value : 32Completed : finished
2. Menggunakan Operator assign(to:on:)
Operator assign secara otomatis akan mengubah nilai dari suatu property tiap kali menerima data baru dari Publisher. kita bisa menentukan property tersebut dengan cara memberikan Key Path pada parameter to, dan memberikan object-nya pada parameter on.
Operator assign1class User {2 var balance = 03}45let user = User()6let balance = CurrentValueSubject<Int, Never>(0)79balance.assign(to: \.balance, on: user)1011balance.send(5000)1213print(user.balance)
Output15000
Tiap kali kita membuat sebuah Subscription menggunakan Operator sink, dia akan mengembalikan sebuah instance dari AnyCancellable yang dinamakan dengan Subscription Token. Kita dapat menyimpan Subscription Token tersebut untuk mengontrol kapan sebuah Subscription harus diberhentikan menggunakan method cancel().
Saat method cancel() dipanggil, memory dan resource yang digunakan oleh subscription tersebut akan di-release sehingga bisa digunakan kembali.
Baca juga : Memory Management pada Swift
Jika kita tidak memanggil method cancel() sebenarnya tidak masalah juga sih, karena instance dari AnyCancellable secara otomatis akan memanggil method cancel() saat deinitialized.
Ada 2 cara untuk menyimpan Subscription Token :
Menyimpan langsung pada variabel.
Store directly to variable1let publisher = Just(3)24let subscription = publisher.sink(receiveCompletion: {5 print("Completed : \($0)")6}, receiveValue: {7 print("Value : \($0)")8})
Menggunakan Operator store(in:)
Operator store2var subscriptions = Set<AnyCancellable>()3let publisher = Just(3)45publisher.sink(receiveCompletion: {6 print("Completed : \($0)")7}, receiveValue: {8 print("Value : \($0)")9})11.store(in: &subscriptions)
Backpressure adalah cara kita untuk mengangani Publisher yang mengirimkan terlalu banyak data kepada Subscriber yaitu dengan cara membatasi data yang masuk yang disebut dengan Demand.
Ada 3 Jenis Demand pada Combine, yaitu :
Demand.none
Demand.max(value: Int)
Demand.unlimited -> default
Untuk menggunakan Backpressure, salah satu caranya adalah dengan membuat sebuah Custom Subscriber. Berikut adalah contoh penggunaan Backpressure :
Backpressure1class CountSubscriber: Subscriber {2 typealias Input = Int3 typealias Failure = Never4 var subscription: Subscription?56 func receive(subscription: Subscription) {7 self.subscription = subscription8 subscription.request(.max(1))9 }1011 func receive(_ input: Int) -> Subscribers.Demand {12 print("- Received : \(input)")13 return Subscribers.Demand.none14 }1516 func receive(completion: Subscribers.Completion<Never>) {}17}1819let subject = PassthroughSubject<Int, Never>()20let subscriber = CountSubscriber()21subject.subscribe(subscriber)22subject.send(1)23subject.send(2)24subject.send(3)
Output1- Received : 1
Pada contoh di atas, kita membatasi Demand maksimal 1. Jadi, meskipun kita panggil method send sampai 3 kali atau bahkan 1000 kali, data yang diterima oleh Subscriber hanya yang pertama saja.
Jika kita mau, Kita juga bisa untuk mengubah Demand, namun sifatnya Additive, artinya Demand yang baru akan ditambahkan dengan Demand yang lama.
Backpressure - Adjust Demand12class CountSubscriber: Subscriber {3 typealias Input = Int4 typealias Failure = Never5 var subscription: Subscription?67 func receive(subscription: Subscription) {8 self.subscription = subscription9 subscription.request(.max(1))1012 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {13 self.subscription?.request(.max(2))14 }16 }1718 func receive(_ input: Int) -> Subscribers.Demand {19 print("- Received : \(input)")20 return Subscribers.Demand.none21 }2223 func receive(completion: Subscribers.Completion<Never>) {}24}252627let subject = PassthroughSubject<Int, Never>()28let subscriber = CountSubscriber()29subject.subscribe(subscriber)30subject.send(1)31subject.send(2)32subject.send(3)3335DispatchQueue.main.asyncAfter(deadline: .now() + 1) {36 subject.send(4)37 subject.send(5)38 subject.send(6)39}
Output1- Received : 12// after 1 second :3- Received : 44- Received : 5
Operator adalah method yang dapat kita gunakan untuk memanipulasi data yang akan diterima oleh Subscriber dari Publisher. Perbedaan Operator dengan method biasa adalah Operator akan me-return sebuah Publisher, jadi bisa dibilang bahwa Operator itu adalah "Re-Publisher" sehingga kita dapat gunakan secara chaining seperti ini :
Operator1[1, 2, 3, 4, 5].publisher2 .filter { $0 % 2 != 0 }3 .map { "Odd number : \($0)" }4 .sink { print($0) }
Output1Odd number : 12Odd number : 33Odd number : 5
Masih ingat dengan Program Reactive yang ada pada bagian awal artikel ini? Yup, pada contoh tersebut saya menggunakan Operator CombineLatest, CombineLatest berfungsi untuk mengkombinasikan Suatu Publisher dengan Publisher tambahan yang diberikan. dan akan mem-publish sebuah Tuple yang mana nilainya adalah data terbaru dari kedua Publisher tersebut.
Reactive Program1let price = CurrentValueSubject<Int, Never>(5000)2let balance = CurrentValueSubject<Int, Never>(0)34let canBuy = balance5 .combineLatest(price)6 .map { $0 >= $1 }78// canBuy -> false910balance.send(5000)1112// canBuy -> true
Pada contoh di atas, kita mengkombinasikan Publisher balance dengan Publisher price menggunakan Operator combineLatest, setelah itu kita transform datanya menggunakan Operator map menjadi tipe data Boolean, untuk mengecek apakah saldo-nya cukup untuk membeli suatu produk atau tidak.
Oke mungkin itu saja yang bisa saya bagikan kali ini, kalau kamu merasa artikel ini bermanfaat silakan Like & Share artikel ini ke teman-teman kamu atau jika kamu punya pertanyaan, tulis aja di kolom komentar, Thank you! 😁 🙏
Referensi :
Always open to new ideas. 🕊️
Articles that you might want to read.
Tutorial penggunaan UIViewRepresentable dan UIViewControllerRepresentable.
Miskonsepsi tentang Tipe Data Symbol
Cheatsheet untuk menerapkan Design System pada Material UI