Without a doubt, Swift Concurrency has revolutionized the way we handle asynchronous code in Swift. One powerful component of it is AsyncStream
, a specialized form of AsyncSequence
that is well-suited to achieve callback- or delegate-like behavior using the async/await syntax.
Prior to Swift Concurrency, developers had to rely on closures to trigger callbacks and inform callers about certain events during asynchronous operations. However, with the introduction of AsyncStream
, this closure-based approach can now be replaced with a more intuitive and straightforward async/await syntax.
In this article, let’s explore a simple yet illustrative example of how AsyncStream
can be utilized to track the progress of a download operation. By the end of this reading, you will have a good understanding of how AsyncStream
works and start using it in your own projects.
So, without further ado, let’s get started.
The Sample App
In order to showcase the power of AsyncStream
, let’s create a sample app that will simulate a download operation and display the download progress using a progress bar.
To emulate the waiting period typically associated with a file download, I have created a File
struct with a performDownload()
method that will sleep for a random amount of duration.
struct File {
let name: String
func performDownload() async {
// Sleep for a random amount of time to emulate the wait required to download a file
let downloadTime = Double.random(in: 0.03...0.5)
try? await Task.sleep(for: .seconds(downloadTime))
}
}
In a real-life situation, this performDownload()
method will most likely consist of code that connects to a server and waits for its response.
With that explanation out of the way, let’s delve into the interesting part.
Creating an AsyncStream
First of all, let’s create a downloader class (FileDownloader
) that accept an array of File
object and download them one by one, and after each successful download, it will inform the caller by providing the filename of the downloaded file.
To achieve such a behavior, the closure-based approach most likely looks something like this:
static func download(_ files: [File], completion: (String) -> Void) {
// Download each file and trigger completion handler
// ...
// ...
}
However, if we opt for the async/await syntax, we will need to replace the completion handler with an AsyncStream
.
static func download(_ files: [File]) -> AsyncStream<String> {
// Init AsyncStream with element type = `String`
let stream = AsyncStream(String.self) { continuation in
// Perform download operation and yield the downloaded file's filename
// ...
// ...
}
return stream
}
As shown in the code above, we can initialize an AsyncStream
by giving it an element type and a custom closure that yields elements to the AsyncStream
. In our case, we will set the element type as String
because our closure will yield the downloaded file’s filename every time a download is successful.
With that in place, we can implement the custom closure that performs the download operation like so:
// Init AsyncStream with element type = `String`
let stream = AsyncStream(String.self) { continuation in
Task {
for file in files {
// Download the file
await file.performDownload()
// Yield the element (filename) when download is completed
continuation.yield(file.name)
}
// All files are downloaded
// Call the continuation’s finish() method when there are no further elements to produce
continuation.finish()
}
}
One essential point to remember while using an AsyncStream
is to call the continuation’s finish()
method after completing all the operations. This step is crucial as failure to do so would lead to indefinite waiting at the call site, causing unintended and undesirable behavior in our apps.
Consuming the AsyncStream
With the FileDownloader
in place, it’s time to integrate it with the user interface to display download progress. To get started, we’ll create 50 File
objects and trigger the download process.
let totalFile = 50
// Generate file objects
let files = (1...totalFile).map { File(name: "Image_\($0).jpg") }
// Start download
let downloaderStream = FileDownloader.download(files)
Now, to display the download progress on the UI, we’ll utilize the given AsyncStream
instance (downloaderStream
), and leverage the for
–await
–in
syntax to process each filename
as the stream produces it when the continuation’s yield()
method being called.
Task {
var downloadedFile = 0
for await filename in downloaderStream {
downloadedFile += 1
// Update progress bar
progressBar.progress = Float(downloadedFile) / Float(totalFile)
// Update status label
statusLabel.text = "Downloaded \(filename)"
}
statusLabel.text = "Download completed"
}
As mentioned earlier, calling the continuation’s finish()
method is an essential step. Without this step, the for loop will be awaiting indefinitely, and the status message will not change to “Download completed”.
If you’d like to try this out yourself, you can find the full sample code on GitHub.
Further Readings
- How Does Swift Concurrency Prevent Thread Explosions?
- The Actor Reentrancy Problem in Swift
- Preventing Data Races Using Actors in Swift
- Understanding Swift Task Groups With Example
- How to Handle Errors in Swift Task Groups
I hope you find this article helpful. Feel free to follow me on Twitter and LinkedIn and subscribe to my newsletter so that you won’t miss out on any of my upcoming articles.
Thanks for reading. 👨🏻💻
👋🏻 Hey!
While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.
- ✨ Bartender: Superpower your menu bar and take full control over your menu bar items.
- ✨ CleanShot X: The best screen capture app I’ve ever used.
- ✨ PixelSnap: Measure on-screen elements with ease and precision.
- ✨ iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.