Swift 中的嵌套异步调用

Nested async calls in Swift

总的来说,我是编程新手,所以我有这个可能很简单的问题。实际上,写作可以帮助我更快地发现问题。

无论如何,我有一个带有多个异步调用的应用程序,它们是这样嵌套的:

InstagramUnoficialAPI.shared.getUserId(from: username, success: { (userId) in

    InstagramUnoficialAPI.shared.fetchRecentMedia(from: userId, success: { (data) in

        InstagramUnoficialAPI.shared.parseMediaJSON(from: data, success: { (media) in

                guard let items = media.items else { return }
                self.sortMediaToCategories(media: items, success: {

                    print("success")

          // Error Handlers

看起来很糟糕,但这不是重点。一旦我开始工作,我将调查 Promise Kit。

我需要 sortMediaToCategories 等待完成,然后重新加载我的 collection 视图。但是,在 sortMediaToCategories 中我有另一个嵌套函数,它也是异步的并且有一个 for in 循环。

func sortMediaToCategories(media items: [StoryData.Items],
                           success: @escaping (() -> Swift.Void),
                           failure: @escaping (() -> Swift.Void)) {

    let group = DispatchGroup()
    group.enter()


    for item in items {


        if item.media_type == 1 {

            guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}

            mediaToStorageDistribution(withImageUrl: url,
                                       videoUrl: nil,
                                       mediaType: .jpg,
                                       takenAt: item.taken_at,
                                       success: { group.notify(queue: .global(), execute: {

                                        self.collectionView.reloadData()
                                           group.leave()

                                       })  },
                                       failure: { print("error") })

 //....

显然我负担不起每次都重新加载 collection 视图,所以我需要等待循环完成然后重新加载。

我正在尝试使用 Dispatch Groups,但遇到了困难。你能帮我解决这个问题吗?任何简单的例子和​​任何建议都将不胜感激。

这样就可以reloadData当最后一项调用成功块时

let lastItemIndex = items.count - 1

for(index, item) in items.enumerated() {

if item.media_type == 1 {

  guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}

  mediaToStorageDistribution(withImageUrl: url,
                             videoUrl: nil,
                             mediaType: .jpg,
                             takenAt: item.taken_at,
                             success: { 
                               if index == lastItemIndex {
                                  DispatchQueue.global().async {

                                    self.collectionView.reloadData()
                                  }         
                                } 
                             },
                             failure: { print("error") })
  }

您面临的问题很常见:​​有多个异步任务并等待所有任务完成。

有几个解决方案。最简单的是利用 DispatchGroup:

func loadUrls(urls: [URL], completion: @escaping ()->()) {
    let grp = DispatchGroup()

    urls.forEach { (url) in
        grp.enter()
        URLSession.shared.dataTask(with: url) { data, response, error in
            // handle error
            // handle response
            grp.leave()
        }.resume()
    }

    grp.notify(queue: DispatchQueue.main) {
        completion()
    }
}

函数 loadUrls 是异步的,需要一个 URL 数组作为输入和一个完成处理程序,当所有任务完成时将调用该处理程序。这将通过 DispatchGroup 来完成,如图所示。

关键是,确保 grp.enter() 调用任务之前被调用, grp.leave 在任务 调用时被调用已完成enterleave 应该是平衡的。

grp.notify 最终注册了一个闭包,当 DispatchGroup grp 平衡(即其内部计数器达到零)时,将在指定的调度队列(此处:main)上调用该闭包。

不过,此解决方案有一些注意事项:

  • 所有任务将几乎同时启动并且运行并发
  • 此处未显示通过完成处理程序报告所有任务的最终结果。它的实施将需要适当的同步。

对于所有这些注意事项,都有很好的解决方案,应该利用合适的第三方库来实施。例如,您可以将任务提交给某种 "executer" 类型,它控制同时有多少任务 运行 (匹配 OperationQueue 和异步操作)。

许多 "Promise" 或 "Future" 库简化了错误处理,还可以帮助您通过一次函数调用解决此类问题。

您必须将 group.enter() 调用移动到循环内。进入和离开的呼叫必须平衡。如果您对成功和失败的 mediaToStorageDistribution 函数的回调是独占的,您还需要在失败时离开该组。当所有调用 enter 的块离开组时,通知将被调用。你可能想用 break 替换 guard 语句中的 return ,以跳过缺少 URL 的项目。现在您正在从整个 sortMediaToCatgories 函数中 returning。

func sortMediaToCategories(media items: [StoryData.Items], success: @escaping (() -> Void), failure: @escaping (() -> Void)) {

    let group = DispatchGroup()

    for item in items {
        if item.media_type == 1 {
            guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else { break }

            group.enter()
            mediaToStorageDistribution(withImageUrl: url,
                                       videoUrl: nil,
                                       mediaType: .jpg,
                                       takenAt: item.taken_at,
                                       success: { group.leave() },
                                       failure: {
                                          print("error")
                                          group.leave()
            })
        }
     }

    group.notify(queue: .main) {
        self.collectionView.reloadData()
    }
 }