简体   繁体   English

Swift中嵌套的异步调用

[英]Nested async calls in Swift

I'm kind of new to programming in general, so I have this maybe simple question. 我对编程很新,所以我有一个简单的问题。 Actually, writing helps me to identify the problem faster. 实际上,写作可以帮助我更快地识别问题。

Anyway, I have an app with multiple asynchronous calls, they are nested like this: 无论如何,我有一个多个异步调用的应用程序,它们嵌套如下:

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

Looks horrible, but that's not the point. 看起来很可怕,但那不是重点。 I will investigate the Promise Kit once I get this working. 一旦我开始工作,我将调查Promise Kit。

I need the sortMediaToCategories to wait for completion and then reload my collection view. 我需要sortMediaToCategories等待完成,然后重新加载我的集合视图。 However, in the sortMediaToCategories I have another nested function, which is async too and has a for in loop. 但是,在sortMediaToCategories我有另一个嵌套函数,它也是异步的并且具有for循环。

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") })

 //....

I can't afford the collection view to reload every time obviously, so I need to wait for loop to finish and then reload. 我无法承担集合视图每次显然重新加载,所以我需要等待循环完成然后重新加载。

I'm trying to use Dispatch Groups, but struggling with it. 我正在尝试使用Dispatch Groups,但正在努力解决它。 Could you please help me with this? 你能帮帮我吗? Any simple examples and any advice will be very appreciated. 任何简单的例子和​​任何建议将非常感激。

The problem you face is a common one: having multiple asynchronous tasks and wait until all are completed. 您面临的问题是一个常见的问题:拥有多个异步任务并等待所有任务完成。

There are a few solutions. 有一些解决方案。 The most simple one is utilising DispatchGroup : 最简单的是利用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()
    }
}

The function loadUrls is asynchronous and expects an array of URLs as input and a completion handler that will be called when all tasks have been completed. 函数loadUrls是异步的,并且期望一个URL数组作为输入,并且在完成所有任务时将调用一个完成处理程序。 This will be accomplished with the DispatchGroup as demonstrated. 这将通过DispatchGroup完成,如图所示。

The key is, to ensure that grp.enter() will be called before invoking a task and grp.leave is called when the task has been completed . 关键是,确保调用任务之前调用grp.enter()并在任务完成时调用grp.leave enter and leave shall be balanced. enterleave应平衡。

grp.notify finally registers a closure which will be called on the specified dispatch queue (here: main) when the DispatchGroup grp balances out (that is, its internal counter reaches zero). grp.notify最后注册一个闭包,当DispatchGroup grp平衡时(即,它的内部计数器达到零),将在指定的调度队列(此处为:main)上调用该闭包。

There are a few caveats with this solution, though: 但是,这个解决方案有一些注意事项:

  • All tasks will be started nearly at the same time and run concurrently 所有任务几乎同时启动并同时运行
  • Reporting the final result of all tasks via the completion handler is not shown here. 此处未显示通过完成处理程序报告所有任务的最终结果。 Its implementation will require proper synchronisation. 它的实现需要适当的同步。

For all of these caveats there are nice solutions which should be implemented utilising suitable third party libraries. 对于所有这些警告,有很好的解决方案应该使用合适的第三方库来实现。 For example, you can submit the tasks to some sort of "executer" which controls how many tasks run concurrently (match like OperationQueue and async Operations). 例如,您可以将任务提交给某种“执行者”,该“执行者”控制并发运行的任务数(与OperationQueue和async Operations匹配)。

Many of the "Promise" or "Future" libraries simplify error handling and also help you to solve such problems with just one function call. 许多“Promise”或“Future”库简化了错误处理,并且只需一个函数调用就可以帮助您解决这些问题。

You can reloadData when the last item calls the success block in this way. 当最后一项以这种方式调用成功块时,您可以重新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") })
  }

You have to move the group.enter() call inside your loop. 您必须在循环内移动group.enter()调用。 Calls to enter and leave have to be balanced. 必须平衡进入和离开的呼叫。 If your callbacks of the mediaToStorageDistribution function for success and failure are exclusive you also need to leave the group on failure. 如果您成功和失败的mediaToStorageDistribution函数的回调是独占的,您还需要让该组失败。 When all blocks that called enter leave the group notify will be called. 当所有调用enter的块离开时,将调用group notify。 And you probably want to replace the return in your guard statement with a break, to just skip items with missing URLs. 并且您可能希望用break替换guard语句中的return,以便跳过缺少URL的项目。 Right now you are returning from the whole sortMediaToCatgories function. 现在你从整个sortMediaToCatgories函数返回。

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()
    }
 }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM