如何在Swift中使用AsyncStream创建类似回调的行为
hudson 译 原文
毫无疑问,Swift并发彻底改变了我们在Swift中处理异步代码的方式。它的一个强大组件是AsyncStream,这是一种特殊的AsyncSequence形式,非常适合使用async/await 语法实现回调或类似委托的行为。
在Swift 并发之前,开发人员必须依靠闭包来触发回调,并在异步操作期间通知调用者某些事件。然而,随着AsyncStream的引入,这种基于闭包的方法现在可以被更直观、更直接的async/await语法所取代。
在本文中,让我们探索一个简单而说明性的示例,说明如何利用AsyncStream来跟踪下载操作的进度。阅读完成后,您将很好地了解AsyncStream的工作原理,并开始在自己的项目中使用它。
所以,不用多说,让我们开始吧。
示例应用程序
为了展示AsyncStream的力量,让我们创建一个示例应用程序,该应用程序将模拟下载操作,并使用进度条显示下载进度。
为了模拟通常与文件下载相关的等待期,我创建了一个带有 performDownload() 方法的File结构,该方法将随机睡眠一段时间。
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))
}
}
在现实生活中,这种performDownload() 方法很可能由连接到服务器并等待其响应的代码组成。
有了这个解释,让我们深入研究有趣的部分。
创建异步流
首先,让我们创建一个下载器类(FileDownloader),该类接受File文件对象数组并逐个下载,每次成功下载后,它将通过提供下载文件的文件名来通知调用者。
为了实现这种行为,基于闭包的方法很可能看起来像这样:
static func download(_ files: [File], completion: (String) -> Void) {
// Download each file and trigger completion handler
// ...
// ...
}
然而,如果我们选择async/await语法,我们将需要用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
}
如上述代码所示,我们可以通过给它一个元素类型和自定义闭包来初始化AsyncStream,该闭包将元素交给AsyncStream。在我们的案例中,我们将元素类型设置为String,因为每次下载成功时,我们的闭包将产生下载文件的文件名。
有了这些,我们可以像这样实现执行下载操作的自定义闭包:
// 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()
}
}
使用AsyncStream时要记住的一个要点是在完成所有操作后调用延续的finish() 方法。这一步骤至关重要,因为如果不这样做,将导致在调用点无限期等待,导致我们应用程序中的意外和非预期的行为。
消费AsyncStream
有了FileDownloader,是时候将其与用户界面集成以显示下载进度了。首先,我们将创建50个File对象并触发下载过程。
let totalFile = 50
// Generate file objects
let files = (1...totalFile).map { File(name: “Image_\($0).jpg”) }
// Start download
let downloaderStream = FileDownloader.download(files)
现在,为了在UI上显示下载进度,我们将利用给定的AsyncStream实例(downloaderStream),并利用for-wait-in语法处理每个文件名,文件名是在调用延续的yield()方法时由 AsyncStream生成的。
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”
}
如前所述,调用延续的 finish() 方法是一个必不可少的步骤。如果没有此步骤,for循环将无限期等待,状态消息将不会更改为“下载完成”。
如果您想亲自尝试一下,您可以在GitHub上找到完整的示例代码。
感谢您的阅读。👨🏻💻