【问题标题】:swift wait until the urlsession finished迅速等到 urlsession 完成
【发布时间】:2021-09-26 13:44:55
【问题描述】:

每次我调用这个 api https://foodish-api.herokuapp.com/api/ 我都会得到一张图片。我不想要一张图片,我需要其中的 11 张,所以我进行了循环以获取 11 张图片。 但是我不能做的是在循环完成后重新加载集合视图。

func loadImages() {

    DispatchQueue.main.async {
                for _ in 1...11{
                       let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
                       let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
                           guard let data = data else { return }
                           do {
                               let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
                               print(json!["image"]!)
                               self.namesOfimages.append(json!["image"]!)
                               
                           } catch {
                               print("JSON error: \(error.localizedDescription)")
                           }
                           }.resume()
        }
    }
    self.collectionV.reloadData()
    print("after resume")
}

【问题讨论】:

  • 视情况而定。您想在每次完成时刷新 11 次吗?或者当所有 11 个都完成时。如果是前者,请按照 ahem 的建议,在更新 namesOfimages 之后立即放置。如果是后者,请使用调度组,在每个请求之前调用enter,在完成处理程序中调用leave,然后添加一个将重新加载表的notify 块。这已经被概述了很多次,所以如果这个模式不熟悉,你会发现很多 S.O.关于这个话题的答案。
  • 一些不相关的观察: 1. 外部DispatchQueue.main.async 是多余的,因为dataTask 已经是异步的。 2. 你确实需要这个异步调度到主线程的地方是namesOfimages的更新。您当前正在从URLSession 后台队列更新此内容,引入了数据竞争。您不想在没有同步的情况下从后台线程更新模型属性(或者只是从主线程进行更新,这是最简单的同步方式)。
  • 3.如果执行 11 个网络请求,您可能会像上面那样愉快地同时启动所有 11 个请求,但如果启动的次数远不止这些,您可能希望限制将要启动的并发请求的数量在任何给定的时刻。

标签: swift api uicollectionview urlsession


【解决方案1】:

通常,当我们想知道一系列并发任务(例如这些网络请求)何时完成时,我们会使用DispatchGroup。在网络请求之前调用enter,在完成处理程序中调用leave,并指定一个notify块,例如

/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue

func loadImages(completion: @escaping ([URL]) -> Void) {
    var imageURLs: [Int: URL] = [:]   // note, storing results in local variable, avoiding need to synchronize with property
    let group = DispatchGroup()
    let count = 11

    for index in 0..<count {
        let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
        group.enter()
        URLSession.shared.dataTask(with: url) { data, response, error in
            defer { group.leave() }

            guard let data = data else { return }

            do {
                let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
                imageURLs[index] = foodImage.url
            } catch {
                print("JSON error: \(error.localizedDescription)")
            }
        }.resume()
    }

    group.notify(queue: .main) {
        let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
        completion(sortedURLs)
    }
}

就个人而言,我使用JSONDecoderDecodable 类型来解析JSON 响应,而不是JSONSerialization。 (另外,我发现键名 image 有点误导,所以我将它重命名为 url 以避免混淆,以明确它是图像的 URL,而不是图像本身。)因此:

struct FoodImage: Decodable {
    let url: URL

    enum CodingKeys: String, CodingKey {
        case url = "image"
    }
}

另请注意,以上内容并未更新属性或重新加载集合视图。执行网络请求的例程也不应该更新模型或 UI。我会把它交给调用者,例如,

var imageURLs: [URL]?

override func viewDidLoad() {
    super.viewDidLoad()

    // caller will update model and UI

    loadImages { [weak self] imageURLs in
        self?.imageURLs = imageURLs
        self?.collectionView.reloadData()
    }
}

注意:

  1. DispatchQueue.main.async 不是必需的。这些请求已经异步运行。

  2. 将临时结果存储在局部变量中。 (而且因为URLSession 使用串行队列,我们​​不必担心进一步的同步。)

  3. 不过,调度组notify 块使用.main 队列,因此调用者可以方便地直接更新属性和UI。

  4. 可能很明显,但我是直接解析URL,而不是解析字符串并将其转换为URL

  5. 当同时获取结果时,您无法保证它们完成的顺序。因此,人们通常会以某种与顺序无关的结构(例如字典)来捕获结果,然后在将结果传回之前对其进行排序。

    在这种特殊情况下,顺序并不重要,但我在上面的示例中包含了这种排序前返回模式,因为它通常是所需的行为。

无论如何,这会产生:

【讨论】:

    【解决方案2】:

    如果您想在加载完所有 11 张图片后重新加载一次,您需要使用 DispatchGroup。添加创建组的属性:

    private let group = DispatchGroup()
    

    然后修改你的 loadImages() 函数:

    func loadImages() {
        for _ in 1...11 {
            let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
            group.enter()
            URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
                guard let self = self else { return }
                self.group.leave()
                guard let data = data else { return }
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
                    print(json!["image"]!)
                    self.namesOfimages.append(json!["image"]!)
                } catch {
                    print("JSON error: \(error.localizedDescription)")
                }
            }.resume()
        }
        group.notify(queue: .main) { [weak self] in
            self?.collectionV.reloadData()
        }
    }
    

    一些描述:

    1. 在方法调用上 group.enter() 将被调用 11 次
    2. 每次完成图片下载后,都会调用 group.leave()
    3. 当 group.leave() 将被调用与 group.enter() 相同的计数时,组调用您在 group.notify() 中定义的块

    更多关于DispatchGroup

    请注意,如果您需要同时下载不同组的图像,则需要处理创建和存储不同的 DispatchGroup 对象。

    【讨论】:

      猜你喜欢
      • 2020-05-08
      • 2017-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-24
      • 2021-05-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多