Reputation: 33
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.
Upvotes: 3
Views: 1501
Reputation: 663
private struct PromiseSendable: @unchecked Sendable {
let value: (Result<String, Error>) -> Void
}
func myFunction() {
Future<String, Error> { promise in
let promiseSendable = PromiseSendable(value: promise)
Task {
do {
// do something
promiseSendable.value(.success("example"))
}
catch let error {
promiseSendable.value(.failure(error))
}
}
}
}
Upvotes: 0
Reputation: 31
import Combine
/// Replacement for a Combine Future that requires Sendable Output and Failure types. This is needed due to the fact that a Combine Future.Promise is not @Sendable.
public struct SwiftConcurrencyPublisher<Output: Sendable, Failure: Sendable & Error>: Publisher {
private let subject: Publishers.HandleEvents<PassthroughSubject<Output, Failure>>
public init(body: @Sendable @escaping () async -> Result<Output, Failure>) {
let subject = PassthroughSubject<Output, Failure>()
let task = Task {
switch await body() {
case .success(let success):
subject.send(success)
subject.send(completion: .finished)
case .failure(let failure):
subject.send(completion: .failure(failure))
}
}
self.subject = subject.handleEvents(receiveCancel: { task.cancel() })
}
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
Upvotes: 0
Reputation: 5099
If you want to fix it for Xcode15 / iOS17 too you need to use wrapper:
fileprivate final class FutureResultWrapper<Output, Failure: Error>: @unchecked Sendable {
fileprivate typealias Promise = (Result<Output, Failure>) -> Void
fileprivate let completionResult: Promise
/// Creates a publisher that invokes a promise closure when the publisher emits an element.
///
/// - Parameter attemptToFulfill: A ``Future/Promise`` that the publisher invokes when the publisher emits an element or terminates with an error.
fileprivate init(_ attemptToFulfill: @escaping Promise) {
self.completionResult = attemptToFulfill
}
}
and then use it like this:
let publisher = Future<T, Never> { [weak self] completionResult in
guard let self = self else {
completionResult(.success(object))
return
}
let wrapper = FutureResultWrapper<T, Never>(completionResult)
Task.detached { [weak self] in
await self?.persist(object)
wrapper.completionResult(.success(object))
}
Upvotes: 2
Reputation: 385960
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()
}
Upvotes: 2