Files
swift-composable-architectu…/Sources/ComposableArchitecture/Effects/Animation.swift
Stephen Celis 9abf1e380d Silence SwiftUI sendability warning (#2540)
Fixes #2539.

We expect reducers to be called on the main thread, so the transaction
should hopefully be safe to commute across non-sendable boundaries, but
we should keep this workaround in mind when we do polish TCA's
sendability story in the future.
2023-11-02 12:16:53 -07:00

102 lines
2.8 KiB
Swift

import Combine
import SwiftUI
extension Effect {
/// Wraps the emission of each element with SwiftUI's `withAnimation`.
///
/// ```swift
/// case .buttonTapped:
/// return .run { send in
/// await send(.activityResponse(self.apiClient.fetchActivity()))
/// }
/// .animation()
/// ```
///
/// - Parameter animation: An animation.
/// - Returns: A publisher.
public func animation(_ animation: Animation? = .default) -> Self {
self.transaction(Transaction(animation: animation))
}
/// Wraps the emission of each element with SwiftUI's `withTransaction`.
///
/// ```swift
/// case .buttonTapped:
/// var transaction = Transaction(animation: .default)
/// transaction.disablesAnimations = true
/// return .run { send in
/// await send(.activityResponse(self.apiClient.fetchActivity()))
/// }
/// .transaction(transaction)
/// ```
///
/// - Parameter transaction: A transaction.
/// - Returns: A publisher.
public func transaction(_ transaction: Transaction) -> Self {
switch self.operation {
case .none:
return .none
case let .publisher(publisher):
return Self(
operation: .publisher(
TransactionPublisher(upstream: publisher, transaction: transaction).eraseToAnyPublisher()
)
)
case let .run(priority, operation):
let uncheckedTransaction = UncheckedSendable(transaction)
return Self(
operation: .run(priority) { send in
await operation(
Send { value in
withTransaction(uncheckedTransaction.value) {
send(value)
}
}
)
}
)
}
}
}
private struct TransactionPublisher<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
var upstream: Upstream
var transaction: Transaction
func receive<S: Combine.Subscriber>(subscriber: S)
where S.Input == Output, S.Failure == Failure {
let conduit = Subscriber(downstream: subscriber, transaction: self.transaction)
self.upstream.receive(subscriber: conduit)
}
private final class Subscriber<Downstream: Combine.Subscriber>: Combine.Subscriber {
typealias Input = Downstream.Input
typealias Failure = Downstream.Failure
let downstream: Downstream
let transaction: Transaction
init(downstream: Downstream, transaction: Transaction) {
self.downstream = downstream
self.transaction = transaction
}
func receive(subscription: Subscription) {
self.downstream.receive(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
withTransaction(self.transaction) {
self.downstream.receive(input)
}
}
func receive(completion: Subscribers.Completion<Failure>) {
self.downstream.receive(completion: completion)
}
}
}