0

vc1 pushes on vc2 and in vc2.

In vc2 I have an array of strings that I convert to urls which eventually go through a URLSession and the images returned get converted to images with filters. Once I get the filtered image in the URLSession callback I save it to cache.

I want the filtered images that are returned to be displayed in the exact order that is in the array of strings. I followed this answer and use DispatchGroup + Semaphores and everything works fine, the order of the filtered images are returned in the exact order.

The problem I ran into is if the user returns back to vc1 then pushes on vc2, once the code below runs and sees that the cache has the first filtered image ("str1"), it adds it to the filteredArray and jumps to dispatchGroup.notify. The filtered images associated with "str1" from the array is the only thing that is appended and the images from "str2" and "str3" aren't.

The cache might or might not have purged the filtered images associated with "str2", "str3" and if it did purge them then the loop should continue to the URLSession callback but it doesn't. But if it didn't purge them it also doesn't add them. That is where my problem occurs, the loop stops running once the cache is hit. This is my first time using a semaphore so I'm not sure where the issues is arising from.

What I did to get around the problem was I simply removed the cache and everything works fine but that also means I don't get the benefit of using the cache.

let imageCache = NSCache<AnyObject, AnyObject>()

let arrOfStringsAsUrls = ["str1", "str2", "str3", "maybe more strings..."]

var filteredArray = [UIImage]()

let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {

    for str in arrOfStringsAsUrls {

        dispatchGroup.enter()

        guard let url = URL(string: str) else {

            dispatchSemaphore.signal()
            dispatchGroup.leave()
            return
        }

        if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage {

            self?.filteredArray.append(cachedImage)

            dispatchSemaphore.signal()
            dispatchGroup.leave()
            return
        }

        URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in

             if let error = error {       
                 dispatchSemaphore.signal()
                 dispatchGroup.leave()
                 return
             }

             guard let data = data, let image = UIImage(data: data) else {
                 dispatchSemaphore.signal()
                 dispatchGroup.leave()
                 return
             }

             DispatchQueue.main.async{ [weak self] in

                 let filteredImage = self?.addFilterToImage(image)

                 self?.imageCache.setObject(image, forKey: str as AnyObject)

                 self?.filteredArray.append(filteredImage)

                 dispatchSemaphore.signal()
                 dispatchGroup.leave()
             }
       }).resume()

       dispatchSemaphore.wait()
    }

    dispatchGroup.notify(queue: dispatchQueue) { in
        // reloadData() and do some other stuff on the main queue
    }
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256

1 Answers1

0

During the first push of the second VCs all images are not in cache so no guard else triggers and also if let cachedImage = imageCache... so everything goes smoothly , after the next push the cache has images stored so this if let triggers

if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage {
  ....
  return
}

and since you put a return statement , it will cause the exit of the for-loop and function , causing the subsequent catches of images to end which triggers the disptach group notify as there is an equal count of enter / leave at that moment , what you really need is when there is an image stored in cache or url is wrong to stop it's fetch with session and the best thing for this job inside a for-loop is continue keyword which will skip the current iteration and jumps to the next one

guard let url = URL(string: str) else { 
   dispatchSemaphore.signal()
   dispatchGroup.leave()
   continue
}

if let cachedImage = imageCache.object(forKey: str as AnyObject) as? UIImage { 
    self?.filteredArray.append(cachedImage) 
    dispatchSemaphore.signal()
    dispatchGroup.leave()
    continue
}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • Sorry, I just noticed you changed everything to continue. But yeah I initially thought it was the return but then I realized all the guard statements have return in them. I’ll try it with continue in about 20 min when I get back to my computer. Question, doesn’t continue mean to continue on... Wouldn’t that defeat the purpose of stopping because something is wrong. For eg in the first guard statement if the url is formatted incorrectly why continue? – Lance Samaria Apr 28 '19 at 22:12
  • I initially used nested if lets (before adding the semaphore and cache) but it was a ugly code that got worse when adding the semaphore so I went with guard that was cleaner. It looks like maybe that’s the only way??? – Lance Samaria Apr 28 '19 at 22:17
  • `continue` inside for loop means to skip the current iteration and jump to the next , you use `return` inside `guard`'s `else` when you need to terminate any coming operations not inside a for loop , `if-let` is another way to go with but you don't have to guard will do the job – Shehata Gamal Apr 28 '19 at 22:44
  • 1
    Ok thanks. I’ve actually never saw anyone use continue with a guard statement. Learn something new everyday! . I’ll try it in a few. It looks like it should work though – Lance Samaria Apr 28 '19 at 22:46
  • I’ve also never used continue before. I never had a need for it – Lance Samaria Apr 28 '19 at 22:47
  • Thanks for adding the additional info! – Lance Samaria Apr 29 '19 at 02:43