swiftcombineswift-concurrency

Mixing Task with Future Publisher in Swift 6 with strict concurrency checking


I implemented a method that returns a Future (a Combine Publisher), which produces a value (or error) from a Task, it works in Swift 5:

  func call(api: API) -> AnyPublisher<Data?, Error> {

    return Future { promise in

      Task {
        let response = await AF.request( ... ).serializingData().response 

          if let error = response.error {
            promise(.failure(error))
          } else {
            promise(.success(response.data))
          }

      }

    }.eraseToAnyPublisher()

  }

The above code could not compile in Swift 6 with strict concurrency checking, because promise is NOT a sendable type:

Capture of 'promise' with non-sendable type '(Result<Data?, any Error>) -> Void' in a `@Sendable` closure

Does this mean with Swift 6, it is no longer possible to have a Combine publisher that produce value from an asynchronous method?

I checked Apple's documentation Using Combine for Your App’s Asynchronous Code, but it does not mention how to mix Combine and Task with strict concurrency checking.


Solution

  • I believe the lack of a @Sendable annotation on Future.Promise is a bug, as I believe it is supposed to be thread-safe. You could file a feedback report with Apple.

    For now, you can work around it using the Swift 6 nonisolated(unsafe) syntax to tell the compiler not to worry about it:

    func call() -> AnyPublisher<Data?, Error> {
        return Future { promise in
    
            // Copy promise to a local property to make it nonisolated(unsafe):
            nonisolated(unsafe) let promise = promise
    
            Task {
                // Now capture promise in a Sendable closure.
    
                promise(.success(Data()))
            }
        }.eraseToAnyPublisher()
    }