0

HI there I am writing a function to get data from my parse server in swift everything is working alright and the data is getting read well. but when I try to return the array it returns me an empty array. I also added a print in the "Get data in background" and there, the array was getting full.So the problem is not getting the data.

public func getthearray() -> Array<UIImage>
{
    let user = PFUser.current()
    let array = user?["Photos"] as! Array<PFFileObject>
    var imagearray = [UIImage]()

    for x in array
    {
        x.getDataInBackground
        { (dataa, error) in

            var img = UIImage(data: dataa!)
            imagearray.append(img!)
        }
    }

    return imagearray
}
Witek Bobrowski
  • 3,749
  • 1
  • 20
  • 34
  • Your problem is async concept. If you add a print when you do `imagearray.append(img!)`, and a print just before `return imagearray` which print will appear first? Which one did you think would have appeared first? – Larme Jan 13 '21 at 09:55
  • 1
    You need `DispatchGroup` and a completion handler, see https://stackoverflow.com/questions/45484563/completion-gets-called-soon/45485143#45485143 – vadian Jan 13 '21 at 10:10

2 Answers2

0

The reason the function returns an empty array is the fact that the code in the x.getDataInBackground completion gets called asynchronously, and before any image will get added to the array, the faction will already return.

Here is the solution (in 2 versions), which is your function slightly refactored and rewritten with completion handler and Grand Central Dispatch

Approach 1. Semaphores

public func getTheArray(_ completion: @escaping ([UIImage]) -> Void) {
    let user: PFUser = .current()
    let array = user?["Photos"] as? [PFFileObject] ?? []
    var result = [UIImage]()

    let semaphore = DispatchSemaphore(value: 0)
    // dispatch to a separate thread which we can safely occupy and block
    // while waiting for the results from async calls.
    DispatchQueue.global().async {
        for file in array {
            file.getDataInBackground { (data, error) in
                if let data = data, let image = UIImage(data: data) {
                    result.append(image)
                }
                // the result have arrived, signal that we are ready
                // to move on to another x
                semaphore.signal()
            }

            // wait until the `signal()` is called, thread is blocked
            semaphore.wait()
        }
        // dispatch to main queue where we are allowed to perform UI updates
        DispatchQueue.main.async {
            // call the completion, but we are doing it only once,
            // when the function is finished with its work
            completion(result)
        }
    }
}

Approach 2. Dispatch Groups

public func getTheArray(_ completion: @escaping ([UIImage]) -> Void) {
    let user: PFUser = .current()
    let array = user?["Photos"] as? [PFFileObject] ?? []
    var result = [UIImage]()

    let group = DispatchGroup()
    for file in array {
        group.enter()
        file.getDataInBackground { (data, error) in
            if let data = data, let image = UIImage(data: data) {
                result.append(image)
            }
            group.leave()
        }
    }

    // notify once all task finish with `leave()` call.
    group.notify(queue: .main) {
        // call the completion, but we are doing it only once,
        // when the function is finished with its work
        completion(result)
    }
}

Usage

and you would call it like this

getTheArray { result in
    // do what you want with result which is your array of UIImages
}

Documentation:

Relevant blogposts:

Witek Bobrowski
  • 3,749
  • 1
  • 20
  • 34
  • Cool. By the way, note that your dispatch group approach won’t preserve the order of the images. If order matters, we’d often store the interim results in a dictionary and then build the properly sorted array in the notify block. – Rob Jan 14 '21 at 00:33
0

You should use the completion handler closure as discussed by others, in order to asynchronously return the array of images when all the requests are done.

The general pattern is:

  1. Use @escaping completion handler closure.
  2. Use dispatch group to keep track of all of the asynchronous requests.
  3. Store the results in a dictionary as the individual requests finish. We use a dictionary for the results, because with concurrent requests, you do not know in what order the requests will finish, so we will store it in a structure from which we can efficiently retrieve the results later.
  4. Use dispatch group notify closure, to specify what should happen when all the requests are done. In this case, we will build a sorted array of images from our unsorted dictionary, and call the completion handler closure.
  5. As an aside, one should avoid using forced unwrapping operator (the ! or as!), especially when dealing with network requests, whose success or failure is outside of your control. We would generally use guard statement to test that the optionals were safely unwrapped.

Thus:

func fetchImages(completion: @escaping ([UIImage]) -> Void) {
    guard
        let user = PFUser.current(),
        let files = user["Photos"] as? [PFFileObject]
    else {
        completion([])
        return
    }

    var images: [Int: UIImage] = [:]                         // dictionary is an order-independent structure for storing the results
    let group = DispatchGroup()                              // dispatch group to keep track of asynchronous requests

    for (index, file) in files.enumerated() {
        group.enter()                                        // enter the group before asynchronous call
        file.getDataInBackground { data, error in
            defer { group.leave() }                          // leave the group when this completion handler finishes

            guard
                let data = data,
                let image = UIImage(data: data)
            else {
                return
            }

            images[index] = image
        }
    }

    // when all the asynchronous tasks are done, this `notify` closure will be called

    group.notify(queue: .main) {
        let array = files.indices.compactMap { images[$0] } // now rebuild the ordered array
        completion(array)
    }
}

And, you'd use it like so:

fetchImages { images in
    // use `images` here, e.g. if updating a model object and refreshing the UI, perhaps:

    self.images = images
    self.tableView.reloadData()
}

// but not here, because the above runs asynchronously (i.e. later)

But the idea is to embrace asynchronous patterns given that we are calling an asynchronous API, and use dispatch groups to keep track of when they are all done. But dispatch semaphores are generally discouraged, as they force the requests to be performed sequentially, one after another, which will slow it down (e.g., in my experience, between two and five times slower).


As an aside, we would generally want to report success or failure. So rather than [UIImage], we would have the closure’s parameter to be a Result<[UIImage], Error> (if you want a single Error) or [Result<UIImage, Error>] if you want either a UIImage or Error for each file. But there’s not enough here to know what error handling you want. But just a FYI.


While I've answered the tactical question above, we really should ask how many images might you be dealing with. If you are retrieving lots of images and/or the user is on a slow connection, this pattern (known as “eager” fetching of the images) is discouraged. First, it can be slow if there are a lot of them. Second, images take up a lot of memory, and you might not want to load all of them if you only need a subset of them at any given point in time. We would often refactor our app to employ “lazy” fetching of the images, retrieving them as needed, not all up front. Or, if you want to do eager fetch, download the assets as files to caches folder (reducing RAM consumption) and create UIImage instances when required in the UI.

Rob
  • 415,655
  • 72
  • 787
  • 1,044