Bir kullanıcı arama çubuğuna yazıyor. Her tuşa basışında API çağrısı yapmak istemezsin - debounce lazım. Sonuçlar geldiğinde duplicate'leri filtrelemen gerek. Hata olursa retry etmeli. Ve tüm bunlar main thread'de UI'ı güncellemeli. Bu senaryoyu imperative kodla yazmak kabus, ama Combine ile 5 satır.
İçindekiler
- Combine Nedir ve Neden Önemli?
- Publisher, Subscriber ve Subscription
- Built-in Publishers
- Operators: Combine'ın Süper Gücü
- Error Handling
- Combine + SwiftUI Entegrasyonu
- Combine vs async/await: Hangisi Ne Zaman?
- Production Patterns
Combine Nedir ve Neden Önemli? {#combine-nedir}
Combine, Apple'ın iOS 13+ ile sunduğu first-party reactive programming framework'ü. RxSwift'in Apple versiyonu diyebilirsin ama daha type-safe ve performanslı.
Temel fikir: zaman içinde değişen değerleri bir pipeline üzerinden işle.
Kavram | Açıklama | Gerçek Hayat Benzetmesi |
|---|---|---|
**Publisher** | Değer üreten kaynak | Su kaynağı |
**Operator** | Değeri dönüştüren ara işlem | Su filtresi |
**Subscriber** | Değeri alan son nokta | Musluk |
**Subscription** | Bağlantı | Su borusu |
Dış Kaynaklar:
Publisher, Subscriber ve Subscription {#temel-kavramlar}
swift
1import Combine2 3// Publisher - değer yayınlar4let numbersPublisher = [1, 2, 3, 4, 5].publisher5 6// Subscriber - değer alır7let cancellable = numbersPublisher8 .sink(9 receiveCompletion: { completion in10 switch completion {11 case .finished:12 print("Bitti!")13 case .failure(let error):14 print("Hata: \(error)")15 }16 },17 receiveValue: { value in18 print("Değer: \(value)")19 }20 )21 22// AnyCancellable - subscription'ı hayatta tutar23// cancellable = nil yapılırsa subscription iptal olur24 25// Custom Publisher26class TemperatureSensor {27 let temperatureSubject = CurrentValueSubject<Double, Never>(20.0)28 29 // Dışarıya sadece Publisher olarak expose et30 var temperaturePublisher: AnyPublisher<Double, Never> {31 temperatureSubject.eraseToAnyPublisher()32 }33 34 func updateTemperature(_ temp: Double) {35 temperatureSubject.send(temp)36 }37}Subject Türleri
Subject | Başlangıç Değeri | Kullanım |
|---|---|---|
**PassthroughSubject** | Yok | Event-driven (buton tıklama, notification) |
**CurrentValueSubject** | Var | State yönetimi (mevcut değer önemli) |
Built-in Publishers {#built-in-publishers}
swift
1// 1. Just - tek değer yayınlar2let just = Just(42)3 4// 2. Future - async işlem sonucu5let future = Future<User, Error> { promise in6 APIService.fetchUser { result in7 promise(result)8 }9}10 11// 3. Timer - periyodik değer12let timer = Timer.publish(every: 1.0, on: .main, in: .common)13 .autoconnect()14 15// 4. NotificationCenter16let keyboardPublisher = NotificationCenter.default17 .publisher(for: UIResponder.keyboardWillShowNotification)18 19// 5. @Published property20class UserSettings: ObservableObject {21 @Published var username = "" // Otomatik publisher!22}23 24// 6. URLSession25let dataPublisher = URLSession.shared26 .dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)Operators: Combine'ın Süper Gücü {#operators}
Operator'lar, publisher pipeline'ında veriyi dönüştüren ara işlemler. Combine'ın gerçek gücü burada.
swift
1// Gerçek dünya örneği: Arama sistemi2class SearchViewModel: ObservableObject {3 @Published var searchText = ""4 @Published private(set) var results: [SearchResult] = []5 6 private var cancellables = Set<AnyCancellable>()7 8 init() {9 $searchText // Publisher<String, Never>10 .debounce(for: .milliseconds(300), // 300ms bekle (her tuşta çağırma)11 scheduler: RunLoop.main)12 .removeDuplicates() // Aynı değeri tekrar gönderme13 .filter { $0.count >= 2 } // Min 2 karakter14 .flatMap { query in // API çağrısı15 SearchService.search(query: query)16 .catch { _ in Just([]) } // Hata durumunda boş dizi17 }18 .receive(on: DispatchQueue.main) // UI thread'e geç19 .assign(to: &$results) // Sonuçları ata20 }21}22 23// Önemli Operatörler24let pipeline = publisher25 .map { $0 * 2 } // Dönüştür26 .filter { $0 > 10 } // Filtrele27 .compactMap { Int($0) } // nil'leri at28 .removeDuplicates() // Tekrarları at29 .debounce(for: .seconds(1), // Gecikme30 scheduler: RunLoop.main)31 .throttle(for: .seconds(2), // Hız sınırla32 scheduler: RunLoop.main,33 latest: true)34 .combineLatest(otherPublisher) // İki publisher'ı birleştir35 .merge(with: anotherPublisher) // Akışları birleştir36 .zip(pairedPublisher) // Eşleştir37 .scan(0) { sum, value in // Kümülatif toplam38 sum + value39 }Error Handling {#error-handling}
swift
1enum APIError: Error {2 case networkError3 case decodingError4 case unauthorized5}6 7// Error handling operators8let request = URLSession.shared.dataTaskPublisher(for: url)9 .tryMap { data, response -> Data in10 guard let httpResponse = response as? HTTPURLResponse else {11 throw APIError.networkError12 }13 guard httpResponse.statusCode == 200 else {14 if httpResponse.statusCode == 401 {15 throw APIError.unauthorized16 }17 throw APIError.networkError18 }19 return data20 }21 .decode(type: [Product].self, decoder: JSONDecoder())22 .mapError { error -> APIError in23 if error is DecodingError {24 return .decodingError25 }26 return error as? APIError ?? .networkError27 }28 .retry(2) // 2 kez tekrar dene29 .catch { error -> Just<[Product]> in30 print("Final error: \(error)")31 return Just([]) // Fallback değer32 }33 .receive(on: DispatchQueue.main)34 .eraseToAnyPublisher()Combine + SwiftUI Entegrasyonu {#swiftui-entegrasyon}
swift
1struct ProductListView: View {2 @StateObject private var viewModel = ProductListViewModel()3 4 var body: some View {5 List(viewModel.products) { product in6 ProductRow(product: product)7 }8 .searchable(text: $viewModel.searchText)9 .overlay {10 if viewModel.isLoading {11 ProgressView()12 }13 }14 }15}16 17class ProductListViewModel: ObservableObject {18 @Published var searchText = ""19 @Published private(set) var products: [Product] = []20 @Published private(set) var isLoading = false21 22 private var cancellables = Set<AnyCancellable>()23 24 init() {25 setupSearch()26 }27 28 private func setupSearch() {29 $searchText30 .debounce(for: .milliseconds(300), scheduler: RunLoop.main)31 .removeDuplicates()32 .sink { [weak self] query in33 Task { await self?.search(query: query) }34 }35 .store(in: &cancellables)36 }37 38 @MainActor39 private func search(query: String) async {40 isLoading = true41 defer { isLoading = false }42 43 do {44 if query.isEmpty {45 products = try await ProductService.fetchAll()46 } else {47 products = try await ProductService.search(query: query)48 }49 } catch {50 products = []51 }52 }53}Combine vs async/await: Hangisi Ne Zaman? {#combine-vs-async}
Senaryo | Combine | async/await |
|---|---|---|
**Tek seferlik API çağrısı** | ❌ Overkill | ✅ Doğal |
**Zaman içinde değişen stream** | ✅ Doğal | ⚠️ AsyncSequence |
**Debounce/throttle** | ✅ Built-in | ❌ Manuel |
**Multiple publishers birleştirme** | ✅ combineLatest, merge | ⚠️ TaskGroup |
**UI binding** | ✅ @Published | ✅ @Observable |
**Error retry** | ✅ .retry(3) | ❌ Manuel loop |
💡 Pro Tip: İkisini birlikte kullan! Combine pipeline'ı, async/await fonksiyonlarını çağırabilir. "Stream" işlemleri için Combine, "one-shot" işlemleri için async/await.
Production Patterns {#production-patterns}
🔑 Bu Yazıdan Çıkarımlar
- Publisher/Subscriber modeli zaman içinde değişen veri akışları için ideal
- Operator chain ile karmaşık veri transformasyonları temiz kalır
- AnyCancellable ile memory leak önle - Set<AnyCancellable> kullan
- Combine + async/await birlikte en güçlü kombinasyon
- Error handling pipeline'da catch, retry, replaceError ile yönet
- @Published SwiftUI ile doğal entegrasyon sağlar
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

