Examples of Using Combine Framework in Swift

Explore practical examples of using Combine framework for reactive programming in Swift.
By Jamie

Introduction to Combine Framework

The Combine framework, introduced by Apple in Swift, provides a declarative Swift API for processing values over time. It enables developers to work with asynchronous events and data streams in a functional way, making it easier to manage complex data flows and UI updates in applications. This guide presents three practical examples of using the Combine framework for reactive programming in Swift, illustrating its capabilities and use cases.

1. Simple Data Fetching with Combine

In this example, we’ll demonstrate how to use Combine to perform a network request and handle the response asynchronously. This is useful for fetching data from APIs and updating the UI accordingly.

import Combine
import Foundation

struct User: Codable {
    let id: Int
    let name: String
}

let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: [User].self, decoder: JSONDecoder())
    .receive(on: RunLoop.main)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished fetching data.")
        case .failure(let error):
            print("Error fetching data: \(error)")
        }
    }, receiveValue: { users in
        print("Fetched users: \(users)")
    })

In this example, we used dataTaskPublisher to create a publisher that fetches data from a URL. We then map the response to extract the data, decode it into an array of User objects, and finally, update the UI on the main thread. The sink method allows us to handle the success and error cases appropriately.

Notes:

  • Always remember to store the cancellable reference to avoid premature cancellation of the publisher.

2. Combining Multiple Publishers

This example illustrates how to combine multiple publishers to reactively update a UI element based on user input and a network response. This is particularly useful for scenarios where you need to react to multiple asynchronous events.

import Combine
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var searchTextField: UITextField!
    @IBOutlet weak var resultLabel: UILabel!

    var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        let searchPublisher = searchTextField.publisher(for: .editingChanged)
            .map { ($0 as? UITextField)?.text ?? "" }
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()

        let apiPublisher = searchPublisher
            .flatMap { query in
                URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/search?query=\(query)")!)
                    .map { $0.data }
                    .decode(type: [String].self, decoder: JSONDecoder())
                    .catch { _ in Just([]) }
            }
            .receive(on: RunLoop.main)
            .sink(receiveValue: { results in
                self.resultLabel.text = results.joined(separator: ", ")
            })

        searchPublisher
            .sink(receiveValue: { query in
                print("Searching for: \(query)")
            })
            .store(in: &cancellables)
    }
}

In this case, we create a publisher from a text field that emits changes when the user types. We debounce the input to avoid making too many requests, remove duplicates, and then use flatMap to fetch data from an API based on the current search query. The results are displayed in a label as the user types.

Notes:

  • debounce is crucial for optimizing API calls, especially for search functionalities.
  • The catch operator is used to handle errors gracefully by returning an empty array if the API call fails.

3. Timer Publisher for Periodic Tasks

This example shows how to use the Combine framework to create a timer publisher that performs a periodic task, such as updating a label every second. This is useful for scenarios like countdown timers or live data updates.

import Combine
import UIKit

class TimerViewController: UIViewController {
    @IBOutlet weak var timerLabel: UILabel!

    var cancellable: AnyCancellable?
    var timerCount: Int = 60

    override func viewDidLoad() {
        super.viewDidLoad()

        cancellable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                if self?.timerCount ?? 0 > 0 {
                    self?.timerCount -= 1
                    self?.timerLabel.text = "\(self?.timerCount ?? 0) seconds remaining"
                } else {
                    self?.timerLabel.text = "Time's up!"
                    self?.cancellable?.cancel()
                }
            }
    }
}

In this example, we create a timer that publishes an event every second. We use the autoconnect method to automatically start the timer, and in the sink closure, we update the timer count and label accordingly. When the timer reaches zero, we cancel the publisher.

Notes:

  • This pattern is useful for creating timers, countdowns, or periodic updates in a user interface.
  • Always ensure to cancel any ongoing subscriptions when they are no longer needed to prevent memory leaks.