Composable Architecture - Return an Effect within the closure

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

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

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?

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

Hey, yes, the future is available. Any help appreciated, in the meantime I will try myself too

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'

You don't need to call sink. Map the response data to actions and then erase.

1 Like

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()

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

You can try using mapError operator.

1 Like

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

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

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.

The catchToEffect with transform was introduced in v0.24.0. Feel free to upgrade if possible :)

1 Like

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

Currently, I am not quite sure if we can support the Effect fully, I will have a look. thanks for the response anyway!!