Exploring Modern Concurrency in Swift - Part4
Introduction
Swift’s new concurrency tools not only simplify working with asynchronous tasks but also provide mechanisms to bridge older, callback-based code with modern async/await patterns. In today’s article, we will explore async continuation for bridging delegate or closure-based APIs and async stream for handling sequences of values asynchronously.
Using Async Continuation
Async continuation provides a way to convert callback-based APIs into async/await compatible functions. This is particularly useful for dealing with legacy code or third-party libraries that do not yet support Swift’s concurrency model.
Example: Bridging a Delegate-based API
Consider an API that uses a delegate to notify when a task is completed. We can convert this to an async function using withCheckedContinuation.
protocol DataFetcherDelegate {
func didFetchData(_ data: String)
}
class DataFetcher {
var delegate: DataFetcherDelegate?
func fetchData() {
// Simulate an asynchronous data fetch
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
self.delegate?.didFetchData("Fetched data")
}
}
}
extension DataFetcher {
func fetchDataAsync() async -> String {
return await withCheckedContinuation { continuation in
self.delegate = DelegateWrapper(continuation: continuation)
self.fetchData()
}
}
private class DelegateWrapper: DataFetcherDelegate {
let continuation: CheckedContinuation<String, Never>
init(continuation: CheckedContinuation<String, Never>) {
self.continuation = continuation
}
func didFetchData(_ data: String) {
continuation.resume(returning: data)
}
}
}
// Usage
let dataFetcher = DataFetcher()
Task {
let data = await dataFetcher.fetchDataAsync()
print(data) // Output: "Fetched data"
}
Here, we define a DelegateWrapper that conforms to DataFetcherDelegate. It captures the continuation and resumes it when the delegate method is called.
Example: Bridging a Closure-based API
Similarly, we can bridge a closure-based API to an async function.
class NetworkManager {
func fetchData(completion: @escaping (String) -> Void) {
// Simulate an asynchronous network call
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
completion("Network data")
}
}
}
extension NetworkManager {
func fetchDataAsync() async -> String {
return await withCheckedContinuation { continuation in
self.fetchData { data in
continuation.resume(returning: data)
}
}
}
}
// Usage
let networkManager = NetworkManager()
Task {
let data = await networkManager.fetchDataAsync()
print(data) // Output: "Network data"
}
In this example, withCheckedContinuation is used to wrap the closure-based callback and convert it into an async function.
Using Async Stream
Async stream provides a way to handle sequences of values asynchronously. This is useful for dealing with continuous streams of data, such as incoming messages or sensor data.
Example: Creating an Async Stream
Once we register for a specific timer we may receive none, one, or many Notifications that fit our criteria. This means we can't use the strategies from the previous section where our closure-based method was replaced with an async call that received exactly one value. CheckedContinuation was required to resume() once and only once.
The continuations you'll encounter in this section may be used one time but they may also be used many times or not at all. In this example you'll meet AsyncStream which is an async type that can deliver zero or more values of a given type in an async context.
class DataProvider {
private var timer: Timer?
func startEmitting() -> AsyncStream<Int> {
return AsyncStream { continuation in
var count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
continuation.yield(count)
count += 1
if count > 5 {
continuation.finish()
self.timer?.invalidate()
}
}
}
}
}
// Usage
let dataProvider = DataProvider()
Task {
for await value in dataProvider.startEmitting() {
print("Received value: \(value)")
}
}
In this example, AsyncStream is used to create a stream of integers emitted every second. The continuation.yield method is used to send values into the stream, and continuation.finish is called to close the stream.
An AsyncStream conforms to the AsyncSequence protocol. Think of an AsyncSequence as a sequence that delivers values over time. That means that we might just iterate over values in the Sequence using fast enumeration while awaiting for the next element.
Conclusion
Swift’s concurrency tools, including async continuation and async stream, provide powerful mechanisms to bridge callback-based code with modern async/await patterns and handle asynchronous sequences of values. By leveraging these tools, you can modernize your codebase, making it more readable and maintainable while taking full advantage of Swift’s concurrency capabilities.