Ali Madhoun
Ali Madhoun

Reputation: 17

swift wait until the urlsession finished

each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images. But what i can't do is reloading the collection view once the loop is finish.

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

Upvotes: 1

Views: 2390

Answers (2)

Rob
Rob

Reputation: 438102

My original answer, outlining the legacy “dispatch group” pattern, is found at the end of this answer. But dispatch groups (and GCD more generally) is a bit of an anachronism nowadays, and instead, we would use Swift concurrency task group, e.g., something like:

nonisolated func loadImages() async -> [Int: URL] {
    await withTaskGroup(of: (index: Int, url: URL?).self) { group in
        let count = …
        for index in 0..<count {
            group.addTask {
                do {
                    let url = …
                    let (data, _) = try await URLSession.shared.data(from: url)
                    let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
                    return (index, foodImage.url)
                } catch {
                    return (index, nil)
                }
            }
        }

        return await group.reduce(into: [:]) { $0[$1.index] = $1.url }
    }
}

There are lots of variations on the theme, but the idea is that nowadays one would use Swift concurrency, not GCD, to manage these concurrent tasks.

My original GCD-based answer is below.


Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.

/// 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 = …
        group.enter()
        URLSession.shared.dataTask(with: url) { data, response, error in
            defer { group.leave() }

            guard let 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)
    }
}

Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:

struct FoodImage: Decodable {
    let url: URL

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

Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,

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

Note:

  1. The DispatchQueue.main.async is not necessary. These requests already run asynchronously.

  2. Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)

  3. The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.

  4. Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.

  5. When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.

    In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.

Anyway, that yields:

enter image description here

Upvotes: 2

loverap007
loverap007

Reputation: 139

If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:

private let group = DispatchGroup()

Then modify your loadImages() function:

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

Some description:

  1. On the method call group.enter() will be called 11 times
  2. On each completion of image downloading group.leave() will be called
  3. When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()

More about DispatchGroup

Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.

Upvotes: 0

Related Questions