jmgawecki
(Kuba Gawecki)
1
Hi there everyone.
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
Thank you kindly.
Read the documentation for Effect here: ComposableArchitecture - Effect
Check out the examples for Effect.task, Effect.future and Effect.result. You have to wrap your URLSession code into one of these effects.
1 Like
eimantas
(Eimantas)
3
You need to use URLSession.shared.dataTaskPublisher here. Not sure about the exact code, but something along the lines of:
case .downloadImage(url: let url):
guard let url = url else {
return Effect(value: ImageLoaderAction.getHAFImage(key: state.hafImageKey))
}
return URLSession.shared.dataTaskPublisher(with: url).map { data, _, _ in
guard
let data = data, let image = UIImage(data: data) else {
return ImageLoaderAction.failedImageLoad
}
return ImageLoaderAction.loadedImage(image)
}.eraseToEffect()
Since you can't capture inout state in a closure — you need to return an action with associated value that then can set state.image to that value:
case .loadedImage(let image):
state.image = image
return .none
2 Likes
jmgawecki
(Kuba Gawecki)
4
Hi, thanks for the response.
I am afraid it does not solve the issue:
case .downloadImage(url: let url):
guard let url = url else { return Effect(value: ImageLoaderAction.imageLoadingFailed) }
return Effect.result {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else {
return 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()
}
Although the .task seems to be the most accurate to my case, I can't use it, it says that
Type 'Effect<Output, Failure>' has no member 'task'
TCA that I use is not necessary updated, maybe that's why. Nevertheless, it cannot be updated on my site for some reasons.
Any suggestions?
GrafHubertus
(Johannes Hubert)
5
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()
}
1 Like
jmgawecki
(Kuba Gawecki)
6
Hey, yes, the future is available. Any help appreciated, in the meantime I will try myself too
jmgawecki
(Kuba Gawecki)
7
Hey, thanks for the response.
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'
eimantas
(Eimantas)
8
You don't need to call sink. Map the response data to actions and then erase.
1 Like
jmgawecki
(Kuba Gawecki)
9
Unfortunately there is the issue with converting the failure type, from URLSession.Failure to Never. Can I wrap that return somehow?
guard let url = url else { return Effect(value: ImageLoaderAction.imageLoadingFailed) }
return URLSession.shared.dataTaskPublisher(for: url)
.map({ data, _ in
guard let data = data else {
return ImageLoaderAction.imageLoadingFailed
}
guard let dataImage = UIImage(data: data) else {
return ImageLoaderAction.imageLoadingFailed
}
return ImageLoaderAction.imageLoaded(image: dataImage)
})
.eraseToEffect()
GrafHubertus
(Johannes Hubert)
10
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()
1 Like
eimantas
(Eimantas)
11
You can try using mapError operator.
1 Like
jmgawecki
(Kuba Gawecki)
12
Thank you both for your help. I present the final solution:
case .downloadImage(url: let url):
guard let url = url else { return Effect(value: ImageLoaderAction.imageLoadingFailed) }
return URLSession.shared.dataTaskPublisher(for: url)
.catchToEffect()
.receive(on: RunLoop.main)
.map({ 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()
Any room for the improvement here? Thanks
1 Like
eimantas
(Eimantas)
13
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()
2 Likes
jmgawecki
(Kuba Gawecki)
14
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.
eimantas
(Eimantas)
15
The catchToEffect with transform was introduced in v0.24.0. Feel free to upgrade if possible :)
1 Like
jmgawecki
(Kuba Gawecki)
16
Sadly, not my jurisdiction ;))
1 Like
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
}
Here is a gist with the full working code.
I hope you find this useful and helpful.
3 Likes
jmgawecki
(Kuba Gawecki)
18
Currently, I am not quite sure if we can support the Effect fully, I will have a look. thanks for the response anyway!!