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.
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.
cancellable
reference to avoid premature cancellation of the publisher.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.
debounce
is crucial for optimizing API calls, especially for search functionalities.catch
operator is used to handle errors gracefully by returning an empty array if the API call fails.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.