Combine Framework: Swift'te Reaktif Programlama
Swift'te asenkron veri akışlarını yönetmek için Combine'ı ustalaşın: Publishers, Subscribers, Operators ve iOS uygulamaları için ileri düzey desenler.

Reaktif programlama, iOS uygulamalarında asenkron olayların ve veri akışlarının ele alınma biçimini kökten değiştirir. Apple'ın yerel framework'ü olan Combine, karmaşık veri pipeline'larını orkestre etmek için bildirimsel ve tip güvenli bir yaklaşım sunar. Bu rehber, temel kavramlardan üretime hazır desenlere kadar uzanır.
Combine, iOS 13+ ile birlikte gelir, Apple'ın optimizasyonları sayesinde daha iyi performans sunar ve SwiftUI ile sorunsuz bütünleşir. Yönetilecek dış bağımlılık yoktur.
Combine'ın temel kavramları
Combine üç temel kavram üzerine kuruludur: değer yayan Publishers, bunları alan Subscribers ve veriyi aralarında dönüştüren Operators. Bu mimari, reaktif ve birleştirilebilir veri pipeline'ları kurmaya olanak tanır.
Publisher: veri kaynağı
Publisher, zamana yayılan bir değer dizisini yayınlayabilen bir tiptir. Her Publisher iki ilişkili tip bildirir: yayınlanan değerin tipi (Output) ve olası hata tipi (Failure). Farklı Publisher türleri şöyle oluşturulur:
import Combine
// Just: emits a single value then completes
// Useful for converting a simple value to a Publisher
let singleValue = Just("Hello Combine")
// CurrentValueSubject: stores and emits the current value
// Perfect for representing state that changes over time
let counter = CurrentValueSubject<Int, Never>(0)
// PassthroughSubject: emits values without storing them
// Ideal for one-time events (taps, notifications)
let buttonTaps = PassthroughSubject<Void, Never>()
// Future: emits a single value asynchronously
// Wraps an async operation that returns a result
let asyncOperation = Future<String, Error> { promise in
// Simulate a network call
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success("Data loaded"))
}
}Hatalar için Never tipi, Publisher'ın asla başarısız olamayacağı anlamına gelir. Bu, derleme zamanında verilen ve hata yönetim kodunu basitleştiren bir garantidir.
Subscriber: değerleri almak
Subscriber, değerlerini almak için bir Publisher'a abone olur. sink metodu Subscriber oluşturmanın en yaygın yoludur. İki closure alır: biri hata veya tamamlanma için, diğeri alınan her değer için:
import Combine
// Variable to store subscriptions
// Without this reference, the subscription would be immediately cancelled
var cancellables = Set<AnyCancellable>()
let publisher = ["Swift", "Combine", "iOS"].publisher
// sink() creates a Subscriber that receives values
publisher
.sink(
// Called when the Publisher completes or fails
receiveCompletion: { completion in
switch completion {
case .finished:
print("✅ Completed successfully")
case .failure(let error):
print("❌ Error: \(error)")
}
},
// Called for each emitted value
receiveValue: { value in
print("Received: \(value)")
}
)
// store() keeps a reference to the subscription
.store(in: &cancellables)
// Output:
// Received: Swift
// Received: Combine
// Received: iOS
// ✅ Completed successfullysink() tarafından döndürülen AnyCancellable'ı her zaman saklayın. Referans olmadan abonelik otomatik olarak iptal edilir ve hiçbir değer alınmaz.
Operators ile veriyi dönüştürmek
Operators, Combine'ın kalbidir. Veri akışlarını bildirimsel olarak dönüştürmeye, filtrelemeye ve birleştirmeye olanak tanır. Her Operator yeni bir Publisher döndürür ve böylece zincirlenebilir.
Temel dönüşüm Operators'leri
Dönüşüm Operators'leri yayınlanan her değeri değiştirir. map değerleri dönüştürür, flatMap iç içe Publishers'ı düzleştirir ve compactMap nil değerleri filtreler:
import Combine
var cancellables = Set<AnyCancellable>()
// map: transforms each value
// Equivalent to map on arrays
[1, 2, 3, 4, 5].publisher
.map { $0 * 2 } // Multiply each number by 2
.sink { print("Doubled: \($0)") }
.store(in: &cancellables)
// Output: 2, 4, 6, 8, 10
// compactMap: transforms AND filters out nil
// Useful for optional conversions
["1", "two", "3", "four", "5"].publisher
.compactMap { Int($0) } // Convert to Int, ignore failures
.sink { print("Valid number: \($0)") }
.store(in: &cancellables)
// Output: 1, 3, 5
// flatMap: flattens nested Publishers
// Essential for chaining async operations
struct User { let id: Int; let name: String }
func fetchUser(id: Int) -> AnyPublisher<User, Never> {
// Simulate an API call
Just(User(id: id, name: "User \(id)"))
.delay(for: .milliseconds(100), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
[1, 2, 3].publisher
.flatMap { id in fetchUser(id: id) } // Each ID becomes an API call
.sink { user in print("User: \(user.name)") }
.store(in: &cancellables)Filtreleme Operators'leri
Filtreleme Operators'leri pipeline'dan hangi değerlerin geçeceğini denetler. Gereksiz işlemleri önlemek ve performansı optimize etmek için zorunludur:
import Combine
var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher
// filter: keeps only values that satisfy the condition
numbers
.filter { $0 > 2 } // Keep only numbers > 2
.sink { print("Filtered: \($0)") }
.store(in: &cancellables)
// Output: 3, 3, 3, 4, 5, 5
// removeDuplicates: removes consecutive identical values
numbers
.removeDuplicates() // Eliminate consecutive duplicates
.sink { print("Without duplicates: \($0)") }
.store(in: &cancellables)
// Output: 1, 2, 3, 4, 5
// debounce: waits for a pause before emitting
// Perfect for real-time search
let searchText = PassthroughSubject<String, Never>()
searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates() // Ignore if text hasn't changed
.sink { query in
print("Search: \(query)")
// Launch API call here
}
.store(in: &cancellables)
// Simulate rapid typing
searchText.send("S")
searchText.send("Sw")
searchText.send("Swi")
searchText.send("Swift") // Only "Swift" is emitted after 300msiOS mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Birden fazla Publisher'ı birleştirmek
Gerçek dünya uygulamaları sıklıkla birden fazla veri kaynağını birleştirmek zorundadır. Combine, bu çoklu akışları orkestre etmek için çeşitli Operators sunar.
CombineLatest ve Zip
combineLatest herhangi bir Publisher yayınladığında, diğerlerinin son değerleriyle birleştirerek yayın yapar. zip ise birleştirmeden önce tüm Publisher'ların yayın yapmasını bekler:
import Combine
var cancellables = Set<AnyCancellable>()
// Simulate a form with validation
let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
// combineLatest: combines the latest values from each Publisher
// Emits on every change from either source
Publishers.CombineLatest(email, password)
.map { email, password in
// Validate that email contains @ and password > 6 chars
let isEmailValid = email.contains("@")
let isPasswordValid = password.count >= 6
return isEmailValid && isPasswordValid
}
.sink { isFormValid in
print("Form valid: \(isFormValid)")
}
.store(in: &cancellables)
email.send("user@example.com") // false (password empty)
password.send("123456") // true (both are valid)
// zip: waits for one value from each Publisher before emitting
// Useful for synchronizing parallel operations
let firstAPI = PassthroughSubject<String, Never>()
let secondAPI = PassthroughSubject<Int, Never>()
Publishers.Zip(firstAPI, secondAPI)
.sink { stringValue, intValue in
print("Received pair: \(stringValue), \(intValue)")
}
.store(in: &cancellables)
firstAPI.send("Hello") // No emission, waiting for secondAPI
secondAPI.send(42) // Emits: ("Hello", 42)
firstAPI.send("World") // No emission, waiting for secondAPI
secondAPI.send(100) // Emits: ("World", 100)Akışları birleştiren Merge
merge, aynı tipteki birden fazla Publisher'ı tek bir akışta birleştirir. Değerler, hangi Publisher gönderdiğinden bağımsız olarak yayın sırasına göre gelir:
import Combine
var cancellables = Set<AnyCancellable>()
// Multiple user notification sources
let pushNotifications = PassthroughSubject<String, Never>()
let localNotifications = PassthroughSubject<String, Never>()
let inAppMessages = PassthroughSubject<String, Never>()
// Merge unifies all streams into one
Publishers.Merge3(pushNotifications, localNotifications, inAppMessages)
.sink { message in
// Handle all notifications the same way
print("📬 Notification: \(message)")
}
.store(in: &cancellables)
pushNotifications.send("New message") // 📬 Notification: New message
localNotifications.send("Reminder: meeting") // 📬 Notification: Reminder: meeting
inAppMessages.send("Welcome!") // 📬 Notification: Welcome!Combine'da hata yönetimi
Hata yönetimi, Combine'ın çekirdeğine yerleştirilmiştir. Publisher'ların Failure tipi, tüm hataların ele alındığını derleyicinin doğrulamasına izin verir.
Toparlama stratejileri
Combine, hatalarla başa çıkmak için çeşitli Operators sunar: başka bir Publisher ile değiştirmek için catch, yeniden denemek için retry ve varsayılan değer için replaceError:
import Combine
var cancellables = Set<AnyCancellable>()
enum APIError: Error {
case networkError
case invalidResponse
case serverError(Int)
}
// Simulate an API call that can fail
func fetchData() -> AnyPublisher<String, APIError> {
Fail(error: APIError.networkError)
.eraseToAnyPublisher()
}
// retry: retries N times before propagating the error
fetchData()
.retry(3) // Try up to 3 times
.catch { error -> Just<String> in
// catch: replaces the error with a fallback Publisher
print("Error after 3 attempts: \(error)")
return Just("Cached data") // Fallback value
}
.sink(
receiveCompletion: { _ in },
receiveValue: { print("Result: \($0)") }
)
.store(in: &cancellables)
// replaceError: replaces any error with a fixed value
// Simpler than catch when only a default value is needed
fetchData()
.replaceError(with: "Error - default value")
.sink { print("With fallback: \($0)") }
.store(in: &cancellables)Never tipinde bir Publisher'ı başarısız olabilen bir Publisher'a dönüştürmek için setFailureType(to:), ters yönde dönüştürmek için ise replaceError(with:) veya catch kullanılır.
Combine ile MVVM deseni
Combine, MVVM (Model-View-ViewModel) desenine doğal olarak entegre olur. ViewModel, View'in gözlemlediği Publisher'ları sunar ve veri ile arayüz arasında reaktif bir bağ kurar.
Tam reaktif ViewModel
Arama, yükleme ve hata yönetimi içeren bir kullanıcı listesi için örnek bir ViewModel:
import Combine
import Foundation
// Data model
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
}
// ViewModel with reactive state
final class UserListViewModel: ObservableObject {
// MARK: - Published Properties (observed by SwiftUI)
@Published var users: [User] = [] // User list
@Published var searchQuery: String = "" // Search text
@Published var isLoading: Bool = false // Loading state
@Published var errorMessage: String? // Optional error message
// MARK: - Private Properties
private var cancellables = Set<AnyCancellable>()
private let userService: UserServiceProtocol
// MARK: - Computed Properties
// Filters users based on search query
var filteredUsers: [User] {
guard !searchQuery.isEmpty else { return users }
return users.filter {
$0.name.localizedCaseInsensitiveContains(searchQuery)
}
}
// MARK: - Initialization
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
setupBindings()
}
// MARK: - Private Methods
private func setupBindings() {
// Observe searchQuery changes
// debounce prevents too frequent calls
$searchQuery
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] query in
// Server-side search logic if needed
print("Search updated: \(query)")
}
.store(in: &cancellables)
}
// MARK: - Public Methods
func loadUsers() {
isLoading = true
errorMessage = nil
userService.fetchUsers()
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
}Combine ve URLSession ile servis
URLSession, dataTaskPublisher aracılığıyla Combine'ı yerel olarak destekler. Yeniden kullanılabilir bir ağ servisi şöyle oluşturulur:
import Combine
import Foundation
protocol UserServiceProtocol {
func fetchUsers() -> AnyPublisher<[User], Error>
}
final class UserService: UserServiceProtocol {
private let baseURL = URL(string: "https://api.example.com")!
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = baseURL.appendingPathComponent("users")
return session.dataTaskPublisher(for: url)
// Check HTTP status code
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard 200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
return data
}
// Decode JSON to Swift model
.decode(type: [User].self, decoder: decoder)
// Erase concrete type to return AnyPublisher
.eraseToAnyPublisher()
}
}iOS mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
SwiftUI entegrasyonu
Combine ve SwiftUI güçlü bir ikili oluşturur. Bir ObservableObject'in @Published özellikleri view güncellemelerini otomatik olarak tetikler.
Combine ViewModel ile SwiftUI View
ViewModel'i bir SwiftUI view'ine bu şekilde bağlanır:
import SwiftUI
struct UserListView: View {
// StateObject: creates and owns the ViewModel
@StateObject private var viewModel = UserListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
// Centered loading indicator
ProgressView("Loading...")
} else if let error = viewModel.errorMessage {
// Error view with retry button
VStack(spacing: 16) {
Text("Error: \(error)")
.foregroundStyle(.red)
Button("Retry") {
viewModel.loadUsers()
}
}
} else {
// User list
List(viewModel.filteredUsers) { user in
UserRowView(user: user)
}
}
}
.navigationTitle("Users")
.searchable(text: $viewModel.searchQuery) // Direct binding
.onAppear {
viewModel.loadUsers() // Load on first appearance
}
}
}
}
struct UserRowView: View {
let user: User
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}İleri düzey desenler
İptal ve otomatik temizleme
Abonelik yaşam döngüsünü yönetmek bellek sızıntılarını önlemek için kritiktir. AnyCancellable ile birlikte kullanılan cancellables deseni otomatik temizliği garanti eder:
import Combine
final class DataManager {
// Set of cancellables: automatically cancelled on destruction
private var cancellables = Set<AnyCancellable>()
// Individual cancellable for fine-grained control
private var currentRequest: AnyCancellable?
func startPolling() {
// Timer that emits every 5 seconds
Timer.publish(every: 5, on: .main, in: .common)
.autoconnect() // Starts automatically
.sink { [weak self] _ in
self?.fetchLatestData()
}
.store(in: &cancellables)
}
func fetchLatestData() {
// Cancel the previous request if it exists
currentRequest?.cancel()
currentRequest = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.decode(type: [String].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.sink { data in
print("Data received: \(data)")
}
}
deinit {
// All cancellables are automatically cancelled
print("DataManager destroyed, subscriptions cancelled")
}
}Threading için Schedulers
Schedulers, işlemlerin hangi thread'te yürütüleceğini kontrol eder. Arka plan işleri için subscribe(on:), UI güncellemeleri için ise receive(on:) kullanın:
import Combine
import Foundation
var cancellables = Set<AnyCancellable>()
func loadAndProcessData() -> AnyPublisher<ProcessedData, Error> {
URLSession.shared.dataTaskPublisher(for: apiURL)
// Perform parsing on a background thread
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.map(\.data)
.decode(type: RawData.self, decoder: JSONDecoder())
// Heavy processing on background thread
.map { rawData in
// This expensive operation runs in the background
processData(rawData)
}
// Return to main thread for UI
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}Sonuç
Combine, iOS uygulamalarında asenkron veri akışlarını yönetmek için güçlü ve bildirimsel bir yaklaşım sunar. Öne çıkan noktalar:
✅ Publishers zamana yayılan değerler yayınlar
✅ Subscribers bu değerleri alır ve işler
✅ Operators akışları dönüştürür ve birleştirir
✅ AnyCancellable abonelik yaşam döngüsünü yönetir
✅ SwiftUI ile @Published, otomatik reaktif bağlar oluşturur
✅ Schedulers optimum performans için thread'leri yönetir
Combine'da ustalaşmak; sağlam, sürdürülebilir ve reaktif iOS uygulamaları kurmaya olanak tanır. SwiftUI ile yerel entegrasyonu, onu modern iOS geliştirmenin vazgeçilmez bir aracı yapar.
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
İlgili makaleler

Swift'te Combine vs async/await: Aşamalı Geçiş Desenleri
Swift'te Combine'dan async/await'e geçiş için kapsamlı kılavuz: aşamalı stratejiler, köprüleme desenleri ve iOS kod tabanlarında paradigma birlikte yaşamı.

2026'da iOS Erişilebilirlik Mülakat Soruları: VoiceOver ve Dynamic Type
iOS mülakatlarına hazırlık için kritik erişilebilirlik soruları: VoiceOver, Dynamic Type, semantik trait'ler ve denetimler.

Swift Macros: pratik metaprogramlama örnekleri
Swift Macros için kapsamlı rehber: freestanding ve attached makro oluşturma, swift-syntax ile AST manipülasyonu ve tekrar eden kodu ortadan kaldıran pratik örnekler.