How can I use Dispatch barrier correctly for network call

How can I use Dispatch barrier for updating dictionary photoDownloadTasks which holds key as image url string and completion block to return downloaded image from URLSession?

Please suggest.

    class ImageService {

    static let shared = ImageService()
    let barrierQueue = DispatchQueue(label: "com.download.tasks", attributes: .concurrent)

    typealias completionHandler = (Result<UIImage, ImageFetchError>) -> Void
    var photoDownloadTasks = [String: [completionHandler]]()

    func downloadPhoto(from url: String?, completion: @escaping (Result<UIImage, ImageFetchError>) -> Void) -> Cancellable? {
        
		var cancellable: Cancellable?
 
        guard let urlString = url else { return cancellable }

       // Update dictionary
        barrierQueue.async(flags: .barrier) {
            if self.photoDownloadTasks.keys.contains(urlString) {
                self.photoDownloadTasks[urlString]?.append(completion)
                return nil // ERROR: Unexpected non-void return value in void function
            } else {
                self.photoDownloadTasks[urlString] = [completion]
			}
        }

        guard let url = URL(string: urlString) else {
            completion(.failure(.invalidRequestURL))
            return cancellable
        }

		// networkService.request(url: url)  <=> URLSession.shared.dataTask(url:url)
        cancellable = networkService.request(url: url) { [weak self] result in
            switch result {
            case .success(let data):
                guard let downloadedImage = UIImage(data: data) else {
                    completion(.failure(.invalidImage))
                    return
                }
                
                
                // execute all blocks store in dictionary
        barrierQueue.sync {
guard let completionHandlers = self?.photoDownloadTasks[urlString] else { return }
                for handler in completionHandlers {
                    handler(.success(downloadedImage))
                }
                self?.photoDownloadTasks[urlString] = nil
		}	
                
            
            case .failure(let error):
                completion(.failure(error))
            }
        }

        return cancellable
    }
}

Presumably, you're storing completion handlers in the photoDownloadTasks property so that if multiple calls are made to downloadPhoto for the same URL before the first network request is finished, only a single network request will actually be made.

Note, however, that for all code paths through downloadPhoto—except for when url cannot be parsed into a URL—you're making a network request. You should only make a network request if there are no completion handlers in the photoDownloadTasks dictionary for the given urlString. If there are, you should append the completion handler to the dictionary and exit early without making a network request.

Concurrent dispatch queues are generally not a good choice (see here). Why not simply use a lock? Something like that:

extension NSLocking {
    func perform<T>(whileLocked block: () -> T) -> T {
        self.lock()
        defer { self.unlock() }
        return block()
    }
}

let lock = NSLock()

func downloadPhoto(...) {
    ...
    let shouldContinue = lock.perform {
        if self.photoDownloadTasks.keys.contains(urlString) {
            self.photoDownloadTasks[urlString]?.append(completion)
            return false
        }
        self.photoDownloadTasks[urlString] = [completion]
        return true
    }

    guard shouldContinue else {
        return nil
    }

    ...

    cancellable = networkService.request(url: url) {
        let result = ...

        let completionHandlers = lock.perform {
            let handlers = photoDownloadTasks[urlString]
            photoDownloadTasks[urlString] = nil
            return handlers
        }
        if let handlers = completionHandlers {
            for handler in handlers {
                handler(result)
            }
        }
    }
    return cancellable
}

Disclaimer: written in the browser, may not compile.

1 Like

Thank you so much @tclementdev ! It works perfectly with the NSLock approach you provided.