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.

Swift iOS'ta reaktif programlama için Combine framework rehberi

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.

Neden RxSwift yerine Combine?

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:

PublisherBasics.swiftswift
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:

SubscriberBasics.swiftswift
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 successfully
Bellek sızıntılarına dikkat

sink() 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:

TransformOperators.swiftswift
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:

FilterOperators.swiftswift
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 300ms

iOS 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:

CombiningPublishers.swiftswift
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:

MergePublishers.swiftswift
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:

ErrorHandling.swiftswift
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 ile Error karşılaştırması

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:

UserListViewModel.swiftswift
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:

UserService.swiftswift
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:

UserListView.swiftswift
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:

CancellationPatterns.swiftswift
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:

SchedulerPatterns.swiftswift
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

#combine
#swift
#ios
#reactive
#async

Paylaş

İlgili makaleler