I am trying to return an effect within the action from the inside of the network call. The issue is Unexpected non-void return value in void function - and I am not quite sure how to solve that issue. Any suggestions?
case .downloadImage(url: let url):
guard let url = url else {
return Effect(value: ImageLoaderAction.getImage(key: state.imageKey))
}
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else {
return Effect<ImageLoaderAction, Never>(value: ImageLoaderAction.getImage(key: state.imageKey)) // Unexpected non-void return value in void function
}
guard let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
state.image = image
}
}
task.resume()
return .none
I think Effect.future is more appropriate in your context. Effect.task is for the new async/await API and was recently added to TCA. Can you check if Effect.future is available in your version? If yes, I can give you an example how to implement it in your case.
You can also check out @eimantas solution. You can transform any Publisher into an Effect.
Edit: This would look something like this:
return Effect.future { promise in
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else {
promise(.success(.imageLoadingFailed)))
}
guard let image = UIImage(data: data) else { promise(.success(.imageLoadingFailed)) }
promise(.success(.imageLoaded(image: image)))
}
task.resume()
}
The problem is that I cannot erase dataTaskPublisher to the effect:
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main)
.sink { _ in } receiveValue: { data in
if let dataImage = UIImage(data: data) {
return ImageLoaderAction.imageLoaded(image: dataImage)
} else {
return ImageLoaderAction.imageLoadingFailed
}
}.eraseToEffect() // Value of type 'AnyCancellable' has no member 'eraseToEffect'
you can do catchToEffect() which will transform the failable publisher in a publisher which never fails.
In your map you can then switch between .success() and .failure().
Edit:
return URLSession.shared.dataTaskPublisher(for: url)
.catchToEffect()
.map { result in
switch result {
case .success(let data):
// your old map code here
case .failure:
return ImageLoaderAction.imageLoadingFailed
}
}
.eraseToEffect()
You can use catchToEffect with transform function. I can also see that you have direct dependency on RunLoop.main, I suggest you move it into environment, so that call-site looks like this:
case .downloadImage(url: let url):
guard let url = url else { return Effect(value: ImageLoaderAction.imageLoadingFailed) }
return URLSession.shared.dataTaskPublisher(for: url)
.receive(on: environment.scheduler)
.catchToEffect({ result in
switch result {
case .success((let data, _)):
guard let dataImage = UIImage(data: data) else {
return ImageLoaderAction.imageLoadingFailed
}
return ImageLoaderAction.imageLoaded(image: dataImage)
case .failure:
return ImageLoaderAction.imageLoadingFailed
}
})
.eraseToEffect()
Its not possible to open the closure with .catchToEffect() on the version of the TCA that I am using apparently. But I moved the RunLoop into my environment. Many thanks for the help guys.
Have you considered moving your networking code into its own environment?
This would encourage modularity and encapsulation should you want to reuse that image fetching function and will help a lot with testing. You can create a mock version and just feed in a local image as a resource in the .xcassets folder of your module.
public struct ImageLoaderEnvironment {
public var fetchImage: (URL?) -> Effect<UIImage, ImageLoaderError>
public init(fetchImage: @escaping (URL?) -> Effect<UIImage, ImageLoaderError>) {
self.fetchImage = fetchImage
}
}
extension ImageLoaderEnvironment {
public static var live: ImageLoaderEnvironment = ImageLoaderEnvironment { url in
guard let url = url else {
return Effect(error: ImageLoaderError.imageLoadingFailed)
}
return URLSession.shared.dataTaskPublisher(for: url)
.receive(on: RunLoop.main)
.map(\.data)
.tryMap({ data -> UIImage in
guard let newImage = UIImage(data: data) else {
throw ImageLoaderError.invalidData
}
return newImage
})
.mapError({ ImageLoaderError.message($0.localizedDescription) })
.eraseToEffect()
}
public static var mock: ImageLoaderEnvironment = ImageLoaderEnvironment { url in
guard let url = url,
let localImage: UIImage = .init(named: "demo", in: .module, with: nil)
else {
return Effect(error: ImageLoaderError.imageLoadingFailed)
}
return Effect(value: localImage)
}
}
Also, removing the switch result dance with tryMap works great and you don't have to return an Action for that, as that could be handled by the imageLoaded action, but holding a Result type instead of an UIImage. By doing so, you can handle the success and failure cases in the reducer.
case .imageLoaded(.success(let image)):
state.image = image
return .none
case .imageLoaded(.failure(let error)):
state.image = UIImage(systemName: "exclamationmark.triangle.fill")
return .none
}