如何在Swift中使用AsyncStream创建类似回调的行为

1,706 阅读3分钟

如何在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上找到完整的示例代码。

感谢您的阅读。👨🏻‍💻