-
Hey everyone! I started a new project ~month ago, and it's my first large one with TCA. It builds with SwiftUI and TCA 1.6.0. In my current case, my class required to be class SomeService: ObservableObject {
@Published var foo: Bool = false
var bar: PassthroughSubject<Void, Never> = .init()
}
@Reducer
struct Feature {
struct State: Equatable {
}
enum Action {
}
var body: some Reducer<State, Action> {
Reduce { state, action in
return .none
}
}
} So, I want to store |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 6 replies
-
The short answer is you don't store reference types in state*. Reference types, which can be mutated from afar, are not a good fit for a uni-directional architecture like TCA, which prescribes that state may only be mutated in response to receiving actions. If a dependency you interact with, eg an Apple API, emits a reference type, you need to make a value type you can map it into before returning those values to state. You are correct to notice that these kinds of APIs, either because you need to hold onto some long-lived reference type, or because they emit or take references types in their API, need to be wrapped in the dependency system. You interact with dependencies you've defined almost always in effects. I highly recommend looking through the case studies and examples in this repo to see just how far you can get with only interacting with dependencies in the reducer through effects. Which is to say: you probably don't want to have access to services in your views. Views should be thin, and merely send actions to the reducer, and there you interact with the service. There are some advanced use cases where you'd want to interact with the same dependency in a reducer and a view. An example I can think of is stuff around AVFoundation, where a SwiftUI *Caveat: there are some TCA tools coming, called "shared state", that will allow you to define a different kind of state. This won't be as naive as just storing vanilla reference types in state, but will be a system for defining state that has reference(ish) behavior. Ie, can be mutated from afar independent of an action being sent, but you still want to be able to observe it. Keep an eye on this repo for an announcement of these tools. But they are not available as of 1.8.0. |
Beta Was this translation helpful? Give feedback.
-
FWIW, the advice to "never store reference types in state" is soon to not be 100% true. We just released a beta focused on "shared state" that provides a tool for introducing a reference type into state that is both observable and exhaustively testable. |
Beta Was this translation helpful? Give feedback.
-
I faced with the exact same problem of Dependencyimport AVKit
import Combine
import Dependencies
import DependenciesMacros
import SwiftUI
@DependencyClient
struct AVPlayerClient: Sendable {
var play: () async throws -> Void
var load: (_ url: URL) async throws -> Void
var avPlayer: @MainActor () -> AVPlayer = { AVPlayer() }
@MainActor private static let player = AVPlayer()
@MainActor private static var playerStatusCancellable: AnyCancellable?
@MainActor private static var playerRateCancellable: AnyCancellable?
@MainActor private static var endOfPlayObserver: (any NSObjectProtocol)?
@MainActor
static func clearResources() {
playerStatusCancellable?.cancel()
playerStatusCancellable = nil
playerRateCancellable?.cancel()
playerRateCancellable = nil
guard let endOfPlayObserver else { return }
NotificationCenter.default.removeObserver(
endOfPlayObserver,
name: AVPlayerItem.didPlayToEndTimeNotification,
object: Self.player.currentItem
)
Self.endOfPlayObserver = nil
}
}
enum PlayerError: Error {
case nothingToPlay
case assetLoadFailed
case unknown
}
extension AVPlayerClient: DependencyKey {
static var liveValue: Self {
Self(
play: { @MainActor in
guard Self.player.currentItem != nil else {
throw PlayerError.nothingToPlay
}
do {
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
endOfPlayObserver = NotificationCenter.default.addObserver(
forName: AVPlayerItem.didPlayToEndTimeNotification,
object: Self.player.currentItem,
queue: .main
) { _ in continuation.resume() }
// Handle errors
playerStatusCancellable = Self.player
.publisher(for: \.status)
.removeDuplicates()
.sink {
if $0 == .failed {
continuation.resume(throwing: Self.player.error ?? PlayerError.unknown)
}
}
playerRateCancellable = Self.player
.publisher(for: \.rate, options: [.new])
.removeDuplicates()
.sink { _ in
if Self.player.rate == 0 {
continuation.resume()
}
}
if Self.player.rate == 0 {
Self.player.play()
}
}
clearResources()
} onCancel: {
Task { @MainActor in
// First pause and then clear to avoid continuation leak
Self.player.pause()
clearResources()
}
}
}
catch {
clearResources()
throw error
}
},
load: { @MainActor url in
do {
try await withCheckedThrowingContinuation { continuation in
let item = AVPlayerItem(url: url)
playerStatusCancellable = item.publisher(for: \.status)
.removeDuplicates()
.sink {
switch $0 {
case .readyToPlay:
continuation.resume()
case .failed:
continuation.resume(throwing: item.error ?? PlayerError.assetLoadFailed)
case .unknown:
break
@unknown default:
break
}
}
Self.player.replaceCurrentItem(with: item)
}
clearResources()
}
catch {
clearResources()
throw error
}
},
avPlayer: {
Self.player
}
)
}
}
extension DependencyValues {
var player: AVPlayerClient {
get { self[AVPlayerClient.self] }
set { self[AVPlayerClient.self] = newValue }
}
}
extension AVPlayerClient: EnvironmentKey {
static var defaultValue: AVPlayerClient {
@Dependency(\.player) var player
return player
}
}
extension EnvironmentValues {
var player: AVPlayerClient {
get { self[AVPlayerClient.self] }
set { self[AVPlayerClient.self] = newValue }
}
} Usageimport AVKit
import ComposableArchitecture
import SwiftUI
@Reducer
struct PlayerFeature {
@ObservableState
struct State: Equatable {}
@Dependency(\.player) private var player
enum Action {
case onAppear
}
var body: some ReducerOf<Self> {
Reduce { _, action in
switch action {
case .onAppear:
return
.run { send in
try await player.play()
}
}
}
}
}
struct PlayerView: View {
@Environment(\.player) private var player // not necessary if using AVPlayerClient.player
let store: StoreOf<PlayerFeature>
var body: some View {
VideoPlayer(player: player.avPlayer()) // or simply AVPlayerClient.player and make it non-private
#if os(tvOS)
.frame(width: 1920, height: 1080)
#endif
.onAppear { store.send(.onAppear) }
}
} I haven't finished all the work to find out all the possible leaks yet, so any suggestions on this (and on anything you want of course) are welcome. |
Beta Was this translation helpful? Give feedback.
The short answer is you don't store reference types in state*. Reference types, which can be mutated from afar, are not a good fit for a uni-directional architecture like TCA, which prescribes that state may only be mutated in response to receiving actions. If a dependency you interact with, eg an Apple API, emits a reference type, you need to make a value type you can map it into before returning those values to state.
You are correct to notice that these kinds of APIs, either because you need to hold onto some long-lived reference type, or because they emit or take references types in their API, need to be wrapped in the dependency system. You interact with dependencies you've defined a…