- Published on
Combine-da Publisher va Subscriber-lar — SwiftUI loyihasida amaliy qo'llash
- Authors
- Name
- ShoxruxC
- @iOSdasturchi
Avvalgi videolarda Timer va onReceive-ni View ichida ishlatdik. Bu videoda esa bir qadam oldinga o'tamiz: barcha logika ViewModel-ga ko'chiriladi, va @Published o'zgaruvchilarni o'zimiz subscribe qilamiz.
@Published = Publisher
Bu muhim tushuncha: ViewModel ichidagi har bir @Published o'zgaruvchi — bu publisher. Har yangilanishda u qiymat chiqaradi. Bu qiymatni sink bilan kuzatib, lozim amallarni bajarish mumkin.
class SubscriberViewModel: ObservableObject {
@Published var count: Int = 0
@Published var textFieldText: String = ""
@Published var textIsValid: Bool = false
@Published var showButton: Bool = false
var cancellables = Set<AnyCancellable>()
init() {
setUpTimer()
addTextFieldSubscriber()
addButtonSubscriber()
}
}
1. Timer — ViewModel ichida
Avvalgi videoda timer View-da edi. Endi ViewModel-da:
func setUpTimer() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
// self endi optional emas — xavfsiz foydalanish mumkin
// Timer 10 ga yetganda to'xtatish
if self.count >= 10 {
self.timer?.cancel()
} else {
self.count += 1
}
}
.store(in: &cancellables)
}
View-da onReceive vs ViewModel-da sink
// VIEW-DA — onReceive (avvalgi video)
.onReceive(timer) { value in
currentDate = value
}
// VIEWMODEL-DA — sink
timer
.sink { [weak self] _ in
self?.count += 1
}
.store(in: &cancellables)
onReceive — faqat View-da ishlaydi. ViewModel-da esa sink ishlatiladi.
2. guard let self = self — professional yondashuv
[weak self] ishlatilganda self optional bo'ladi. Har safar self?.count, self?.timer deb yozish o'rniga — bir marta tekshirib, kuchli reference olish:
.sink { [weak self] _ in
// Bu usul — har doim self?. yozmasdan
guard let self = self else { return }
// Endi self optional emas
self.count += 1
self.timer?.cancel()
}
3. cancellables Set — bir nechta publisher boshqaruvi
Bitta AnyCancellable? o'rniga Set<AnyCancellable> — ko'p publisherlarni birga boshqarish uchun:
var cancellables = Set<AnyCancellable>()
// Har bir publisher-ni shu set-ga saqlash
somePublisher
.sink { ... }
.store(in: &cancellables)
// Barchasini bekor qilish
for item in cancellables {
item.cancel()
}
4. TextField subscriber — matn tekshiruvi
Haqiqiy ilovalarda keng tarqalgan holat: foydalanuvchi yozayotgan matn kamida N ta belgi bo'lsin:
func addTextFieldSubscriber() {
$textFieldText // textFieldText publisher-iga subscribe
.map { text in
return text.count > 3 // 3 dan ko'p belgi bo'lsa true
}
.sink { [weak self] isValid in
guard let self = self else { return }
self.textIsValid = isValid
}
.store(in: &cancellables)
}
assign vs sink:
// assign — qisqaroq, lekin weak self yo'q → xotira muammosi
$textFieldText
.map { $0.count > 3 }
.assign(to: \.textIsValid, on: self) // ❌ weak self mumkin emas
.store(in: &cancellables)
// sink — ko'proq kod, lekin weak self bor → professional
$textFieldText
.map { $0.count > 3 }
.sink { [weak self] isValid in // ✅ weak self — to'g'ri yondashuv
self?.textIsValid = isValid
}
.store(in: &cancellables)
Tavsiya: assign qisqaroq ko'rinsa ham, weak self bo'lmaganligi uchun professional kodda sink afzal.
5. View-da validatsiya ko'rsatish
TextField ustida xmark/checkmark ko'rsatish:
TextField("Biror narsa yozing...", text: $vm.textFieldText)
.frame(height: 55)
.padding(.leading)
.background(Color(.systemGray5))
.cornerRadius(10)
.font(.headline)
.overlay(alignment: .trailing) {
ZStack {
// X — matn yetarli emas
Image(systemName: "xmark")
.foregroundColor(.red)
.opacity(
vm.textFieldText.count < 1 ? 0 :
vm.textIsValid ? 0 : 1
)
// Checkmark — matn yetarli
Image(systemName: "checkmark")
.foregroundColor(.green)
.font(.title)
.opacity(vm.textIsValid ? 1 : 0)
}
.padding(.trailing)
}
6. debounce — qidiruv optimallashtirish
Foydalanuvchi tez yozayotganda har harf uchun logika ishlatmaslik uchun — 0.5 soniya kutib, keyin ishlatish:
func addTextFieldSubscriber() {
$textFieldText
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
// 0.5 soniyadan keyin oxirgi qiymat bilan ishlaydi
.map { text in
return text.count > 3
}
.sink { [weak self] isValid in
guard let self = self else { return }
self.textIsValid = isValid
}
.store(in: &cancellables)
}
Debounce qachon kerak:
- Qidiruv maydoni — har harf uchun API so'rov jo'natmaslik
- Murakkab filtrlash — har harf uchun katta hisoblash ishlatmaslik
- Username tekshiruvi — foydalanuvchi yozishni tugatgunicha kutish
7. combineLatest — ikki publisherni birlashtirish
Ikki publisherning oxirgi qiymatlarini birgalikda kuzatish:
func addButtonSubscriber() {
$textIsValid
.combineLatest($count)
// textIsValid yoki count o'zgarganda — ikkalasining oxirgi qiymat bilan ishla
.sink { [weak self] isValid, count in
guard let self = self else { return }
// Matn to'g'ri VA count 10 dan katta bo'lsa Submit tugmasini ko'rsat
if isValid && count >= 10 {
self.showButton = true
} else {
self.showButton = false
}
}
.store(in: &cancellables)
}
Submit tugmasi
Button {
// amal
} label: {
Text("SUBMIT")
.font(.headline)
.foregroundColor(.white)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
.opacity(vm.showButton ? 1.0 : 0.5)
.disabled(!vm.showButton)
Natija: foydalanuvchi 4+ belgi yozib, timer 10-ga yetgandan keyin Submit tugmasi faollashadi.
To'liq ViewModel
import Combine
class SubscriberViewModel: ObservableObject {
@Published var count: Int = 0
@Published var textFieldText: String = ""
@Published var textIsValid: Bool = false
@Published var showButton: Bool = false
var cancellables = Set<AnyCancellable>()
init() {
setUpTimer()
addTextFieldSubscriber()
addButtonSubscriber()
}
func setUpTimer() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
self.count += 1
}
.store(in: &cancellables)
}
func addTextFieldSubscriber() {
$textFieldText
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.map { $0.count > 3 }
.sink { [weak self] isValid in
self?.textIsValid = isValid
}
.store(in: &cancellables)
}
func addButtonSubscriber() {
$textIsValid
.combineLatest($count)
.sink { [weak self] isValid, count in
guard let self = self else { return }
self.showButton = isValid && count >= 10
}
.store(in: &cancellables)
}
}
Xulosa
| Vosita | Maqsad |
|---|---|
@Published | O'zgaruvchini publisher-ga aylantirish |
sink | Publisherga subscribe bo'lish (ViewModel-da) |
onReceive | Publisherga subscribe bo'lish (View-da) |
map | Publisherdan kelgan qiymatni boshqa turga aylantirish |
debounce | Bir qator o'zgarishlarda faqat oxirgini ishlatish |
combineLatest | Ikki publisherning oxirgi qiymatlarini birgalikda olish |
assign | Qiymatni o'zgaruvchiga to'g'ridan-to'g'ri belgilash (weak self yo'q) |
store(in:) | Publisher-ni cancelables-ga saqlash |
Publisher va subscriber yondashuvi — ilova bo'ylab ma'lumotlarni sinxron ushlab turishning eng samarali usuli. @Published o'zgaruvchilar o'zgarsa — barcha kuzatuvchilar avtomatik xabar oladi va ekran yangilanadi.