Combine Framework
Darsliklar (Tutorials)
Combine Framework — To'liq Darslik (Zero to Hero)
Mundarija
- Nega Combine kerak? (Motivatsiya)
- Combine nima?
- Asosiy tushunchalar (Core Concepts)
- Publisher turlari
- Subscriber va Cancellable
- Operatorlar — Combine'ning kuchi
- Networking (API bilan ishlash)
- Combine + SwiftUI integratsiyasi
- Combine + UIKit integratsiyasi
- Subject'lar (PassthroughSubject va CurrentValueSubject)
- Xatolarni boshqarish (Error Handling)
- Backpressure — Senior darajasi
- Combine vs Async/Await — qachon qaysi biri?
- Real loyiha: Qidiruv tizimi (Search)
- Xulosa va keyingi qadamlar
Nega Combine kerak? {#nega-combine-kerak}
Muammo: Asinxron kod — Callback Hell
Tasavvur qiling: siz foydalanuvchi login qilgandan so'ng, uning profilini yuklashingiz, keyin profildagi ID bo'yicha postlarni yuklashingiz, keyin har bir post'dagi rasmlarni yuklashingiz kerak. Combine'siz bu shunday ko'rinadi:
// ❌ YOMON: Callback Hell (Pyramid of Doom)
func loadEverything() {
loginUser(email: "test@mail.com", password: "123") { user, error in
guard let user = user else { return }
fetchProfile(userId: user.id) { profile, error in
guard let profile = profile else { return }
fetchPosts(profileId: profile.id) { posts, error in
guard let posts = posts else { return }
fetchImages(for: posts) { images, error in
guard let images = images else { return }
// Nihoyat! 4 ta ichma-ich (nested) closure
// Bu kodni o'qish, testlash va debug qilish — DAHSHAT!
}
}
}
}
}
Muammolar:
- 😵 Kodni o'qish juda qiyin (piramida shakli)
- 🐛 Xatolarni boshqarish har bir qatlamda alohida yoziladi
- 💀
guardesdan chiqsa — crash - 🔄 Agar foydalanuvchi login oynaga qaytsa — eski so'rovlar to'xtamaydi (memory leak)
- 🧪 Unit test yozish deyarli imkonsiz
Yechim: Combine bilan yozilsa
// ✅ YAXSHI: Combine Pipeline — toza va tushunarli
func loadEverything() {
loginUser(email: "test@mail.com", password: "123")
.flatMap { user in fetchProfile(userId: user.id) }
.flatMap { profile in fetchPosts(profileId: profile.id) }
.flatMap { posts in fetchImages(for: posts) }
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Xatolik: \(error)")
}
},
receiveValue: { images in
// Tayyor! Barcha rasmlar yuklandi
self.images = images
}
)
.store(in: &cancellables)
}
Natija:
- ✅ Kod chiziqli (linear) va o'qish oson
- ✅ Xatoliklar bitta joyda boshqariladi
- ✅
.store(in:)orqali xotira avtomatik tozalanadi - ✅ Unit test yozish oson
Yana bir motivatsiya: Real-time ma'lumotlar
Ba'zi muammolar bir martalik emas, balki uzluksiz oqim (stream) sifatida ishlaydi:
| Vaziyat | Callback bilan | Combine bilan |
|---|---|---|
| Qidiruv maydoni (har harfda API) | 😫 Murakkab timer + cancel logika | ✅ .debounce() — 1 qator |
| Tugmani tez-tez bosish (spam) | 😫 Bool flag + timer | ✅ .throttle() — 1 qator |
| 2 ta API natijasini birlashtirish | 😫 DispatchGroup + mutex | ✅ .combineLatest() — 1 qator |
| Tarmoq uzilganda qayta urinish | 😫 Rekursiv chaqiruv | ✅ .retry(3) — 1 qator |
💡 Xulosa: Combine — bu "oddiy ishlash" emas, "professional ishlash" demak. Agar siz faqat bir martalik API chaqirsangiz,
async/awaitham yetarli. Lekin real loyihalarda stream, debounce, merge, retry kabi ehtiyojlar bo'lganda — Combine hali ham tengsiz.
Combine nima? {#combine-nima}
Combine — Apple tomonidan iOS 13 (2019) da taqdim etilgan Functional Reactive Programming (FRP) framework'i. U asinxron hodisalarni "Pipeline" (zanjir) ko'rinishida yozish imkonini beradi.
Asosiy g'oya
Hayotiy misol bilan tushuntirish:
📡 Publisher (TV stansiya) → 🔧 Operators (Antenna/Filtrlar) → 📺 Subscriber (Televizor)
- Publisher — ma'lumot tarqatadi (kanal signal yuboradi)
- Operators — oradagi filtrlar, transformatorlar (signalni tozalaydi, kuchaytiiradi)
- Subscriber — oxirgi natijani qabul qiladi (televizorda ko'rinadi)
Performance (Ishlash tezligi)
| Framework | Tezlik | Apple native? |
|---|---|---|
| Combine | ⚡ Eng tez | ✅ Ha |
| RxSwift | 🐢 ~41% sekinroq | ❌ Yo'q |
| Async/Await | ⚡ Tez (1 martalik) | ✅ Ha |
Combine C++ darajasida Apple tizimiga to'g'ridan-to'g'ri integratsiya qilingan, shuning uchun uchinchi tomon kutubxonalaridan sezilarli darajada tezroq ishlaydi.
Asosiy tushunchalar (Core Concepts) {#asosiy-tushunchalar}
Combine 3 ta ustunga qurilgan:
1. Publisher — Ma'lumot tarqatuvchi
Publisher — bu vaqt o'tishi bilan qiymatlar yoki xatolik chiqaruvchi tur. U ikkita tur (type) bilan aniqlanadi:
Output— chiqaradigan qiymat turi (masalan,String,Int,[Post])Failure— mumkin bo'lgan xatolik turi (masalan,URLError,Never)
// Publisher protokoli (tuzilishi)
protocol Publisher {
associatedtype Output // Qanday qiymat chiqaradi?
associatedtype Failure: Error // Qanday xato bo'lishi mumkin?
}
2. Subscriber — Obunachi
Subscriber — Publisher'dan kelayotgan ma'lumotlarni qabul qiluvchi. Eng ko'p ishlatiladigan subscriber'lar:
.sink { }— qiymatni closure ichida qabul qilish.assign(to:)— qiymatni to'g'ridan-to'g'ri property'ga bog'lash
3. Cancellable — Xotira boshqaruvchisi
Har bir obuna AnyCancellable qaytaradi. Bu obunani xotirada ushlab turadi. Agar u yo'q qilinsa — obuna ham to'xtaydi. Bu ARC (Automatic Reference Counting) bilan birgalikda xotira oqishining oldini oladi.
// To'g'ri ishlatish
var cancellables = Set<AnyCancellable>()
publisher
.sink { value in print(value) }
.store(in: &cancellables) // ← xotirani boshqaradi
⚠️ Ogohlantirish:
.store(in:)chaqirmay qo'ysangiz, obuna darhol o'chadi va siz hech qanday ma'lumot olmaysiz!
Publisher turlari {#publisher-turlari}
Just — bitta qiymat chiqaradi va tugaydi
let publisher = Just(42)
publisher.sink { value in
print(value) // 42
}
[Array].publisher — massiv elementlarini birma-bir chiqaradi
let publisher = [1, 2, 3, 4, 5].publisher
publisher.sink { value in
print(value)
}
// Natija: 1, 2, 3, 4, 5 (birma-bir)
@Published — o'zgaruvchi o'zgarganda avtomatik signal
class UserViewModel: ObservableObject {
@Published var username: String = ""
}
let vm = UserViewModel()
vm.$username // ← $ belgisi publisher'ni oladi
.sink { newValue in
print("Yangi nom: \(newValue)")
}
.store(in: &cancellables)
vm.username = "Azimjon" // Natija: "Yangi nom: Azimjon"
Timer Publisher — vaqt oralig'ida signal
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { date in
print("Har sekundda: \(date)")
}
.store(in: &cancellables)
NotificationCenter Publisher
NotificationCenter.default
.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { _ in
print("Ilova faol bo'ldi!")
}
.store(in: &cancellables)
Subscriber va Cancellable {#subscriber-va-cancellable}
.sink — universal subscriber
let numbers = [10, 20, 30].publisher
numbers
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("✅ Stream muvaffaqiyatli tugadi")
case .failure(let error):
print("❌ Xatolik: \(error)")
}
},
receiveValue: { value in
print("Qiymat: \(value)")
}
)
.store(in: &cancellables)
.assign(to:) — property'ga to'g'ridan-to'g'ri bog'lash
class ViewModel: ObservableObject {
@Published var score: Int = 0
}
let vm = ViewModel()
// score property'sini publisher bilan bog'laymiz
Just(100)
.assign(to: &vm.$score) // score = 100 bo'ladi
print(vm.score) // 100
Cancellable amaliyoti — obunani bekor qilish
// Obunani qo'lda bekor qilish
let subscription = [1, 2, 3].publisher
.sink { print($0) }
subscription.cancel() // Obuna to'xtaydi, xotira tozalanadi
Operatorlar — Combine'ning kuchi {#operatorlar}
Operatorlar — bu Publisher va Subscriber o'rtasidagi filtrlar va transformatorlar. Ular yordamida ma'lumotni o'zgartirish, filtrlash va birlashtirish mumkin.
Transformatsiya operatorlari
// map — har bir elementni o'zgartirish
[1, 2, 3].publisher
.map { $0 * 10 } // 10, 20, 30
.sink { print($0) }
.store(in: &cancellables)
// compactMap — nil qiymatlarni olib tashlash
["1", "abc", "3"].publisher
.compactMap { Int($0) } // 1, 3 (abc o'tkazilmadi)
.sink { print($0) }
.store(in: &cancellables)
// flatMap — publisher ichidan yangi publisher chiqarish
[1, 2, 3].publisher
.flatMap { id in
fetchUser(id: id) // Har bir ID uchun API chaqiruv
}
.sink { user in print(user.name) }
.store(in: &cancellables)
Filtrlash operatorlari
// filter — shartga mos kelganlarni o'tkazish
[1, 2, 3, 4, 5, 6].publisher
.filter { $0 % 2 == 0 } // 2, 4, 6 (faqat juft sonlar)
.sink { print($0) }
.store(in: &cancellables)
// removeDuplicates — ketma-ket takrorlanganlarni olib tashlash
[1, 1, 2, 2, 3, 1].publisher
.removeDuplicates() // 1, 2, 3, 1
.sink { print($0) }
.store(in: &cancellables)
// first / last — faqat birinchi yoki oxirgi qiymat
[10, 20, 30].publisher
.first() // 10
.sink { print($0) }
.store(in: &cancellables)
Vaqt bilan ishlash operatorlari
// debounce — foydalanuvchi yozishni to'xtatguncha kutish
$searchText
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { text in print("Qidiruv: \(text)") }
.store(in: &cancellables)
// throttle — ma'lum vaqt oralig'ida faqat bitta signal
buttonTapPublisher
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.sink { print("Tugma bosildi") }
.store(in: &cancellables)
// delay — signalni kechiktirish
Just("Salom!")
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { print($0) } // 2 sekunddan keyin "Salom!"
.store(in: &cancellables)
Birlashtirish operatorlari
// combineLatest — ikki publisher'dan eng oxirgi qiymatlarni birlashtrish
let name = PassthroughSubject<String, Never>()
let age = PassthroughSubject<Int, Never>()
name.combineLatest(age)
.sink { name, age in
print("\(name), \(age) yosh")
}
.store(in: &cancellables)
name.send("Ali")
age.send(25) // "Ali, 25 yosh"
name.send("Vali") // "Vali, 25 yosh"
// merge — ikki publisher'ni bitta oqimga birlashtirish
let api1 = fetchFromServerA()
let api2 = fetchFromServerB()
api1.merge(with: api2)
.sink { data in print("Ma'lumot keldi: \(data)") }
.store(in: &cancellables)
// zip — ikki publisher'dan juftlik bo'lib qiymat olish
let letters = ["A", "B", "C"].publisher
let numbers = [1, 2, 3].publisher
letters.zip(numbers)
.sink { letter, number in
print("\(letter)\(number)") // A1, B2, C3
}
.store(in: &cancellables)
Networking (API bilan ishlash) {#networking}
Oddiy API so'rov
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
class PostsViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
@Published var errorMessage: String?
var cancellables = Set<AnyCancellable>()
func fetchPosts() {
isLoading = true
errorMessage = nil
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
URLSession.shared.dataTaskPublisher(for: url)
// 1. Background thread'da JSON'ni parse qilish
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
// 2. Main thread'ga o'tish (UI yangilash uchun)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] posts in
self?.posts = posts
}
)
.store(in: &cancellables)
}
}
Ketma-ket API so'rovlar (Chained Requests)
func loadUserAndPosts(userId: Int) {
let baseURL = "https://api.example.com"
// 1-qadam: Foydalanuvchini yuklash
URLSession.shared.dataTaskPublisher(for: URL(string: "\(baseURL)/users/\(userId)")!)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
// 2-qadam: Foydalanuvchining postlarini yuklash
.flatMap { user in
URLSession.shared.dataTaskPublisher(for: URL(string: "\(baseURL)/users/\(user.id)/posts")!)
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { print("Tugadi: \($0)") },
receiveValue: { posts in
print("Postlar: \(posts.count) ta")
}
)
.store(in: &cancellables)
}
Bir nechta API'ni parallel chaqirish
func loadDashboard() {
let fetchUser = URLSession.shared.dataTaskPublisher(for: userURL)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
let fetchPosts = URLSession.shared.dataTaskPublisher(for: postsURL)
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
// Ikkalasini PARALLEL yuklash va natijalarni birlashtirish
fetchUser.combineLatest(fetchPosts)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Xatolik: \(error)")
}
},
receiveValue: { user, posts in
self.user = user
self.posts = posts
}
)
.store(in: &cancellables)
}
Combine + SwiftUI integratsiyasi {#combine-swiftui}
ViewModel Pattern
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
@Published var message: String = ""
var cancellables = Set<AnyCancellable>()
init() {
// count o'zgarganda avtomatik message yangilanadi
$count
.map { value -> String in
if value == 0 { return "Boshlang!" }
if value > 10 { return "Ajoyib! 🎉" }
return "Davom eting: \(value)"
}
.assign(to: &$message)
}
func increment() { count += 1 }
func decrement() { count -= 1 }
}
// SwiftUI View
struct CounterView: View {
@StateObject var vm = CounterViewModel()
var body: some View {
VStack(spacing: 20) {
Text(vm.message)
.font(.title)
Text("\(vm.count)")
.font(.largeTitle.bold())
HStack {
Button("−") { vm.decrement() }
Button("+") { vm.increment() }
}
.font(.title)
}
}
}
Form Validatsiya (Real-world misol)
class RegistrationViewModel: ObservableObject {
@Published var email = ""
@Published var password = ""
@Published var confirmPassword = ""
@Published var isValid = false
@Published var validationMessage = ""
var cancellables = Set<AnyCancellable>()
init() {
// 3 ta maydonni kuzatib, validatsiyani real-time tekshirish
Publishers.CombineLatest3($email, $password, $confirmPassword)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.map { email, password, confirm -> (Bool, String) in
// Email tekshirish
guard email.contains("@") else {
return (false, "Email noto'g'ri formatda")
}
// Parol uzunligi
guard password.count >= 8 else {
return (false, "Parol kamida 8 ta belgi bo'lishi kerak")
}
// Parollar mos kelishi
guard password == confirm else {
return (false, "Parollar mos kelmaydi")
}
return (true, "✅ Hammasi to'g'ri!")
}
.sink { [weak self] isValid, message in
self?.isValid = isValid
self?.validationMessage = message
}
.store(in: &cancellables)
}
}
Combine + UIKit integratsiyasi {#combine-uikit}
UITextField kuzatish
class SearchViewController: UIViewController {
let searchField = UITextField()
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// UITextField qiymatini Combine orqali kuzatish
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: searchField)
.compactMap { ($0.object as? UITextField)?.text }
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.sink { [weak self] searchText in
self?.performSearch(query: searchText)
}
.store(in: &cancellables)
}
func performSearch(query: String) {
print("Qidiruv: \(query)")
}
}
UIButton publisher
// UIControl uchun Combine Extension
extension UIControl {
func publisher(for event: UIControl.Event) -> AnyPublisher<Void, Never> {
UIControlPublisher(control: self, event: event)
.eraseToAnyPublisher()
}
}
// Ishlatish:
loginButton.publisher(for: .touchUpInside)
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] in
self?.performLogin()
}
.store(in: &cancellables)
Subject'lar {#subjectlar}
Subject'lar — bu Publisher hamda Subscriber sifatida ishlaydigan maxsus turlar. Ular orqali dastur kodidan qo'lda qiymat yuborish mumkin.
PassthroughSubject — qiymatni saqlamaydi
let subject = PassthroughSubject<String, Never>()
subject
.sink { value in print("Qabul qilindi: \(value)") }
.store(in: &cancellables)
subject.send("Salom") // "Qabul qilindi: Salom"
subject.send("Dunyo") // "Qabul qilindi: Dunyo"
subject.send(completion: .finished) // Stream tugadi
💡 Qachon ishlatiladi? Tugma bosilganda signal yuborish, event'larni tarqatish.
CurrentValueSubject — eng oxirgi qiymatni saqlaydi
let subject = CurrentValueSubject<Int, Never>(0) // Boshlang'ich qiymat: 0
print(subject.value) // 0
subject
.sink { value in print("Hozirgi: \(value)") }
.store(in: &cancellables)
subject.send(10) // "Hozirgi: 10"
subject.send(20) // "Hozirgi: 20"
print(subject.value) // 20 — har doim oxirgi qiymatni bilish mumkin
💡 Qachon ishlatiladi? State (holat) boshqaruvi: loading holati, score, counter.
Farqlari jadvalda
| Xususiyat | PassthroughSubject | CurrentValueSubject |
|---|---|---|
| Boshlang'ich qiymat | ❌ Yo'q | ✅ Bor |
.value property | ❌ Yo'q | ✅ Bor |
| Yangi obunachiga oxirgi qiymat | ❌ Bermaydi | ✅ Beradi |
| Ishlatilish joyi | Event/Action | State/Holat |
Xatolarni boshqarish (Error Handling) {#xatolarni-boshqarish}
replaceError — xato bo'lsa default qiymat
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
.replaceError(with: []) // Xato bo'lsa bo'sh massiv
.receive(on: DispatchQueue.main)
.assign(to: &$posts)
retry — avtomatik qayta urinish
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // 3 marta qayta urinadi
.map(\.data)
.decode(type: [Post].self, decoder: JSONDecoder())
.catch { error -> Just<[Post]> in
print("3 urinishdan keyin ham xato: \(error)")
return Just([])
}
.receive(on: DispatchQueue.main)
.assign(to: &$posts)
catch — xatoni ushlash va boshqa publisher'ga o'tish
primaryAPI()
.catch { _ in backupAPI() } // Asosiy xato bosa, zaxira API
.receive(on: DispatchQueue.main)
.assign(to: &$data)
mapError — xato turini o'zgartirish
URLSession.shared.dataTaskPublisher(for: url)
.mapError { urlError -> AppError in
return AppError.networkFailed(urlError.localizedDescription)
}
.sink(
receiveCompletion: { completion in
if case .failure(let appError) = completion {
// Endi AppError turi bilan ishlaysiz
}
},
receiveValue: { _ in }
)
.store(in: &cancellables)
Backpressure — Senior darajasi {#backpressure}
Muammo
Tasavvur qiling: 1000 ta rasmni yuklash kerak. Agar hammasi bir vaqtda yuklanishni boshlasa:
- 📈 RAM sakraydi (maqsadsiz 1000 ta URLSession task ochiladi)
- 💥 OOM (Out of Memory) crash
- 🌐 Server ortiqcha so'rovlardan overload bo'ladi
Yechim: flatMap(maxPublishers:)
func downloadImages(urls: [URL]) {
urls.publisher
// Bir vaqtda FAQAT 3 ta rasm yuklanadi
.flatMap(maxPublishers: .max(3)) { url in
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.replaceError(with: Data())
}
.receive(on: DispatchQueue.main)
.sink { imageData in
print("Rasm yuklandi: \(imageData.count) bytes")
}
.store(in: &cancellables)
}
Natija: 1000 ta rasm xavfsiz va barqaror yuklanadi — RAM spike yo'q, server overload yo'q.
Custom Subscriber bilan Demand boshqaruvi
// Senior-level: Publisher'ga "menga 1tadan ber" deyish
class PaginatedSubscriber: Subscriber {
typealias Input = [Post]
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.max(1)) // Faqat 1 ta sahifa so'rash
}
func receive(_ input: [Post]) -> Subscribers.Demand {
print("Sahifa yuklandi: \(input.count) ta post")
return .max(1) // Yana 1 ta sahifa so'rash
}
func receive(completion: Subscribers.Completion<Never>) {
print("Barcha sahifalar yuklandi")
}
}
Combine vs Async/Await — qachon qaysi biri? {#combine-vs-async-await}
| Vaziyat | Combine | Async/Await |
|---|---|---|
| Bitta API so'rov | ✅ Mumkin | ✅ Afzalroq |
| Ketma-ket 2-3 API | ✅ flatMap | ✅ Afzalroq (linear) |
| Real-time stream (chat, timer) | ✅ Faqat Combine | ❌ AsyncStream kerak |
| Debounce / Throttle | ✅ Faqat Combine | ❌ Qo'lda yozish kerak |
| UI form validatsiya | ✅ Faqat Combine | ❌ Murakkab |
| CombineLatest (2+ oqim) | ✅ Faqat Combine | ❌ TaskGroup boshqa narsa |
| Retry + Error handling | ✅ 1 qator | ✅ Loop bilan mumkin |
💡 Maslahat: Ikkalasini birgalikda ishlating! Oddiy API chaqiruvlarni
async/awaitbilan, murakkab stream/reaktiv logikani Combine bilan yozing.
Real loyiha: Qidiruv tizimi (Search) {#real-loyiha-qidiruv}
Bu — Combine'ning eng kuchli amaliy namunasi. Real ilovadagi qidiruv tizimini yaratamiz:
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [SearchResult] = []
@Published var isSearching = false
var cancellables = Set<AnyCancellable>()
init() {
$searchText
// 1. Foydalanuvchi 0.5 soniya yozishni to'xtatguncha kutish
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
// 2. Oldingi so'z bilan bir xil bo'lsa — o'tkazib yuborish
.removeDuplicates()
// 3. Bo'sh matnni filtrlash
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
// 4. Loading holatini yoqish
.handleEvents(receiveOutput: { [weak self] _ in
self?.isSearching = true
})
// 5. API so'rov — FAQAT eng oxirgi so'rovni bajarish
.map { query in
self.searchAPI(query: query)
.replaceError(with: [])
}
.switchToLatest() // Eski so'rovlarni bekor qiladi!
// 6. Main thread'da UI yangilash
.receive(on: DispatchQueue.main)
.sink { [weak self] results in
self?.results = results
self?.isSearching = false
}
.store(in: &cancellables)
}
private func searchAPI(query: String) -> AnyPublisher<[SearchResult], Error> {
let url = URL(string: "https://api.example.com/search?q=\(query)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [SearchResult].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
Bu bitta pipeline ichida 6 ta muammoni hal qiladi:
- ✅ Server spam bo'lmaydi (
debounce) - ✅ Takroriy so'rov ketmaydi (
removeDuplicates) - ✅ Bo'sh so'rov yuborilmaydi (
filter) - ✅ Loading indikator ko'rinadi (
handleEvents) - ✅ Eski so'rovlar bekor qilinadi (
switchToLatest) - ✅ UI Main thread'da yangilanadi (
receive(on:))
🔥 Bu kodni callback bilan yozish uchun 60+ qator kerak bo'lardi. Combine bilan — 20 qator!
Xulosa va keyingi qadamlar {#xulosa}
Nimalarni o'rgandik?
| Daraja | Mavzu | Holat |
|---|---|---|
| 🟢 Beginner | Publisher, Subscriber, Cancellable | ✅ |
| 🟢 Beginner | Just, @Published, Timer publisher | ✅ |
| 🟡 Middle | Operators (map, filter, debounce, combineLatest) | ✅ |
| 🟡 Middle | Networking (URLSession + Combine) | ✅ |
| 🟡 Middle | SwiftUI va UIKit integratsiyasi | ✅ |
| 🔴 Senior | Subject'lar (Passthrough, CurrentValue) | ✅ |
| 🔴 Senior | Error Handling (retry, catch, mapError) | ✅ |
| 🔴 Senior | Backpressure va Demand | ✅ |
| 🔴 Senior | switchToLatest, Custom Subscriber | ✅ |
Keyingi qadamlar
- Amaliyot: Har bir operatorni Xcode Playground'da sinab ko'ring.
- Real loyiha: Mavjud loyihangizda bitta ViewModel'ni Combine'ga o'tkazing.
- Instruments: Memory Allocations va Time Profiler bilan Combine pipeline'larni tahlil qiling.
- WWDC: "Combine in Practice" (WWDC 2019) videolarni ko'ring.
- Testing: XCTest ichida Combine pipeline'larni test qilishni o'rganing.
🎯 Esda tuting: Combine — bu shunchaki framework emas, bu fikrlash tarzi. Har bir muammoni "oqim" (stream) sifatida ko'rishni o'rganing va siz haqiqiy Senior iOS Engineer bo'lasiz!