I am having trouble to use the result of a completion handler. I am getting this error "Cannot convert value of type '()' to expected argument type"

struct SearchCollectionViewModel {
    let name: String
    let previewURL: String?
    var image:UIImage?
    let dataController = DataController()
}

extension SearchCollectionViewModel {
    init(with result: Result) {
        self.name = result.trackName
        self.previewURL = result.previewURL
        if let url = result.previewURL {
            let imgData = preview(with: url, completion: { data -> Data? in
                guard let data = data as? Data else { return nil }
                return data
            })
            self.image = UIImage(data: imgData)
        }
    }

    private func preview(with url: String, completion: @escaping (Data) -> Data?) {
        dataController.download(with: url) { data, error  in
            if error == nil {
                guard let imageData = data else { return }
                DispatchQueue.main.async {
                    _ = completion(imageData)
                }
            }
        }
    }
}

1 Answers

0
Rob On

A couple of observations:

  1. You cannot “return” a value that is retrieved asynchronously via escaping closure.

  2. The closure definition (Data) -> Data? says that the closure not only will be passed the Data retrieved for the image, but that the closure will, itself, return something back to preview. But it’s obviously not doing that (hence the need for _, as in _ = completion(...)). I’d suggest you change that to (Data?) -> Void (or use the Result<T, U> pattern).

  3. I’d suggest renaming your Result type as there’s a well-known generic called Result<Success, Failure> for returning .success(Success) or .failure(Failure). This is a pattern that we’ve used for a while, but is formally introduced in Swift 5, too. See SE-0235.

    Your codebase can have its own Result type, but it’s an invitation for confusion later down the road if and when you start adopting this Result<T, U> convention.

  4. You really shouldn’t be initiating asynchronous process from init, but instead invoke a method to do that.

  5. Personally, I’d move the conversion to UIImage into the DataController, e.g.

    extension DataController {
        func downloadImage(with url: URL, completion: @escaping (UIImage?, Error?) -> Void) {
            let task = URLSession.shared.dataTask(with: url) { data, _, error in
                let image = data.flatMap { UIImage(data: $0) }
                completion(image, error)
            }
            task.resume()
        }
    }
    

So, I might suggest you end up with something like:

class SearchCollectionViewModel {
    let name: String
    let previewURL: String?
    let dataController = DataController()
    var image: UIImage?

    init(with result: Result) {
        self.name = result.trackName
        self.previewURL = result.previewURL
    }
}

extension SearchCollectionViewModel {
    func preview(with url: String, completion: @escaping (UIImage?) -> Void) {
        guard let urlString = previewURL, let url = URL(string: urlString) else {
            completion(nil)
            return
        }

        dataController.downloadImage(with: url) { [weak self] image, error in
            DispatchQueue.main.async {
                self?.image = image
                completion(image)
            }
        }
    }
}