Loop doesn't complete cycles with dispatch queue, iOS, Swift

Viewed 153

I have a function that runs a loop the trigers another function for each item in the loop but it seems to not run the function as many times as there is items in the array.

Here are my functions.

func startLoop(completion: @escaping (_ finished: Bool) -> ()) {


    print("Tony items amount is \(tempImgUrls.count)")
        for item in tempImgUrls {
            dispatchGroup.enter()
            print("Tony begin loop")
            let img = item["imgUrl"]
            let name = item["name"]
            downloadImages(img: img!, name: name!, completion: { (complete) in
                print("Tony mid loop")
                self.dispatchGroup.leave()
            })
        }

    dispatchGroup.notify(queue: DispatchQueue.main) {
        print("Tony end loop")
        completion(true)
        }
    }

func downloadImages(img: String, name: String, completion: @escaping (_ finished: Bool) -> ()) {


            imageShown.sd_setImage(with: URL(string: img), completed: { (image, error, cacheType, imageUrl) in

                let personImg = image!
                let personId = name
                let post = Person(personImage: personImg, personId: personId)

                self.finalImgUrls.append(post)
                completion(true)
                print("Tony array is with images person is \(self.finalImgUrls)")
                print("Tony items 2 amount is \(self.finalImgUrls.count)")

        })
    }
}

And this is the print out in consol, as you can see it prints the loop start first then the mid 1 time and the end 1 time with one item appended at the end instead of 4 like what is fed in.

Tony items amount is 4

Tony begin loop

Tony begin loop

Tony begin loop

Tony begin loop

Tony mid loop

Tony array is with images person is [AppName.Person]

Tony items 2 amount is 1

2 Answers

The code you are having there is working. The problem seems to be that sd_setImage delivers just once a result.

How to test

If you use a test implementation of sd_setImage as follows, then you get the expected result:

class SomeClass {
    func sd_setImage(with url: URL?, completed: @escaping (UIImage?, Error?, String, URL) -> Void) {
        let random = Double(arc4random()) / Double(UINT32_MAX)
        DispatchQueue.main.asyncAfter(deadline: .now() + random, execute: {
            completed(UIImage(), nil, "a", url!)
        })
    }
}

When you use UIImageView asynchronous image retrieval extensions, they invariably cancel the prior request for that image view when requesting another image for the same image view. (This is generally by design, as it prevents the request for images from being backlogged by requests for prior images for the same image view that are no longer needed, such as an image view in a table view cell that can be reused.)

The problem is that SDWebImage’s UIImageView extension does not call its completion handler if the image retrieval was canceled. This is an API design flaw, IMHO, as asynchronous methods should always call their completion handler (with a “canceled” error code if the request was canceled). But because SDWebImage doesn’t do this, your loop's initial requests are being canceled in lieu of subsequent requests, and as a result, your leave calls for the earlier iterations are never getting called. FYI, other image libraries, such as KingFisher do not suffer from this design flaw (though it has its own issues).

But this begs the question of what your intent was. If you want to see the sequence of images in succession, for example, then you shouldn’t initiate the next image retrieval until the prior one is done (e.g. initiate the next image retrieval in the completion handler of the prior one). Or perhaps you should just retrieve the images yourself (not using the UIImageView extension that cancels prior requests) and do some timed updating of your UI there. But you almost certainly don’t want to initiate multiple concurrent image updates for the same image view.

Related