How to solve @Sendable warnings inside completion block

42 Views Asked by At

I enabled Strict Swift Concurrency Check and get some warnings. I would love to discuss and understand them. This is my code.

class DownloadImageAsyncImageLoader {
    
    let url = URL(string: "https://picsum.photos/200")!
    
    func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
        guard let data = data,
        let image = UIImage(data: data),
        let response = response as? HTTPURLResponse,
              response.statusCode >= 200 && response.statusCode < 300 else {
            return nil
        }
        
        return image
    }
    
    func downloadWithEscaping(completion: @escaping (UIImage?, Error?) -> Void) {
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let image = self?.handleResponse(data: data, response: response) else {
                completion(nil, error)
                return
            }
            
            completion(image, nil)
        }
        .resume()
    }
}

I got two warnings inside URLSession data task callback.

  1. Capture of 'self' with non-sendable type 'DownloadImageAsyncImageLoader?' in a @Sendable closure.

This class has no data at all. I can make it final and conform to Sendable explicitly. Then this warning goes away. If this class cannot be final, what should we do?

  1. Capture of 'completion' with non-sendable type '(UIImage?, (any Error)?) -> Void' in a @Sendable closure

I have no idea how to address this one.

1

There are 1 best solutions below

0
On

I strongly suggest that you write this using Swift Concurrency, instead of using completion handlers. It would look something like this:

func downloadWithEscaping() async throws -> UIImage {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let image = handleResponse(data: data, response: response) else {
        throw Errors.invalidImage
    }
    
    return image
}

enum Errors: Error {
    case invalidImage
}

As for the second error, it is saying that your completion handler is not @Sendable. You can fix the error by just marking it as @Sendable,

func downloadWithEscaping(completion: @escaping @Sendable (UIImage?, Error?) -> Void)

but of course this puts certain restrictions on what you can do in the completion handler (the same restrictions as what you can do in the completion handler of URLSession.shared.dataTask(with: url)), so you might get more errors depending on how you use this method.

For the first error, if your class cannot be final, that means subclasses can add whatever properties they like in a non-sendable way, so capturing self is inherently unsafe.

That said, handleResponse doesn't use any instance properties. It might as well be a static func (or class func if you allow subclasses to override it). Then you can call it with Self.handleResponse(...) instead of self.handleResponse(...).