Exploring Modern Concurrency in Swift - Part4

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.

要查看或添加评论,请登录

AbdelAli J.的更多文章

  • Which property wrapper for what purpose?

    Which property wrapper for what purpose?

    In SwiftUI property wrappers are often overused even though they are powerful tools. A key principle is to use regular…

  • Leave your ego behind

    Leave your ego behind

    Ego is our sense of self-esteem that helps shape our identity and life goals. However an unchecked ego can lead to…

  • How I Interview Developers: A Simple Approach

    How I Interview Developers: A Simple Approach

    When I interview a developer I focus on three important questions: Are they smart? Can they get things done? Can We…

    1 条评论
  • Exploring Modern Concurrency in Swift - Part3

    Exploring Modern Concurrency in Swift - Part3

    Introduction Concurrency means running multiple tasks at the same time. It can be tricky, especially when different…

  • Exploring Modern Concurrency in Swift - Part2

    Exploring Modern Concurrency in Swift - Part2

    Introduction Swift has some great tools for handling concurrency, which means running multiple tasks at the same time…

  • Exploring Modern Concurrency in Swift - Part1

    Exploring Modern Concurrency in Swift - Part1

    Swift introduced robust and integrated tools for handling concurrency, simplifying how we work with asynchronous code…

    1 条评论
  • The Power of Naming Conventions

    The Power of Naming Conventions

    When SwiftUI 2 was released our team eagerly adopted it for our project. We jumped right in and enjoyed the excitement…

  • Understanding SwiftUI’s Observation Framework

    Understanding SwiftUI’s Observation Framework

    In SwiftUI observation tools like @Published and ObservableObject weren’t perfect. They often made us either watch too…

  • Swift Enums tips and tricks

    Swift Enums tips and tricks

    Swift enums are perhaps one of the most interesting features of the language. From defining a finite list of cases, to…

    1 条评论
  • Build, Test, & Deploy an iOS App with Github Actions (Part1)

    Build, Test, & Deploy an iOS App with Github Actions (Part1)

    In his article, you'll learn how to automate the process of deploying your builds to testers using TestFlight. The Goal…

社区洞察

其他会员也浏览了