@preconcurrency import Combine import SwiftUI /// A `ViewStore` is an object that can observe state changes and send actions. They are most /// commonly used in views, such as SwiftUI views, UIView or UIViewController, but they can be used /// anywhere it makes sense to observe state or send actions. /// /// In SwiftUI applications, a `ViewStore` is accessed most commonly using the ``WithViewStore`` /// view. It can be initialized with a store and a closure that is handed a view store and returns a /// view: /// /// ```swift /// var body: some View { /// WithViewStore(self.store, observe: { $0 }) { viewStore in /// VStack { /// Text("Current count: \(viewStore.count)") /// Button("Increment") { viewStore.send(.incrementButtonTapped) } /// } /// } /// } /// ``` /// /// View stores can also be observed directly by views, scenes, commands, and other contexts that /// support the `@ObservedObject` property wrapper: /// /// ```swift /// @ObservedObject var viewStore: ViewStore /// ``` /// /// > Tip: If you experience compile-time issues with views that use ``WithViewStore``, try /// > observing the view store directly using the `@ObservedObject` property wrapper, instead, which /// > is easier on the compiler. /// /// In UIKit applications a `ViewStore` can be created from a ``Store`` and then subscribed to for /// state updates: /// /// ```swift /// let store: Store /// let viewStore: ViewStore /// private var cancellables: Set = [] /// /// init(store: Store) { /// self.store = store /// self.viewStore = ViewStore(store, observe: { $0 }) /// } /// /// func viewDidLoad() { /// super.viewDidLoad() /// /// self.viewStore.publisher.count /// .sink { [weak self] in self?.countLabel.text = $0 } /// .store(in: &self.cancellables) /// } /// /// @objc func incrementButtonTapped() { /// self.viewStore.send(.incrementButtonTapped) /// } /// ``` @available( iOS, deprecated: 9999, message: "Use '@ObservableState', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Using-ObservableState" ) @available( macOS, deprecated: 9999, message: "Use '@ObservableState', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Using-ObservableState" ) @available( tvOS, deprecated: 9999, message: "Use '@ObservableState', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Using-ObservableState" ) @available( watchOS, deprecated: 9999, message: "Use '@ObservableState', instead. See the following migration guide for more information: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Using-ObservableState" ) @dynamicMemberLookup #if swift(<5.10) @MainActor(unsafe) #else @preconcurrency@MainActor #endif public final class ViewStore: ObservableObject { // N.B. `ViewStore` does not use a `@Published` property, so `objectWillChange` // won't be synthesized automatically. To work around issues on iOS 13 we explicitly declare it. public nonisolated let objectWillChange = ObservableObjectPublisher() private let _state: CurrentValueRelay private var viewCancellable: AnyCancellable? #if DEBUG private let storeTypeName: String #endif let store: Store /// Initializes a view store from a store which observes changes to state. /// /// It is recommended that the `observe` argument transform the store's state into the bare /// minimum of data needed for the feature to do its job in order to not hinder performance. /// This is especially true for root level features, and less important for leaf features. /// /// To read more about this performance technique, read the article. /// /// - Parameters: /// - store: A store. /// - toViewState: A transformation of `ViewState` to the state that will be observed for /// changes. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed. public convenience init( _ store: Store, observe toViewState: @escaping (_ state: State) -> ViewState, removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool ) { self.init( store, observe: toViewState, send: { $0 }, removeDuplicates: isDuplicate ) } /// Initializes a view store from a store which observes changes to state. /// /// It is recommended that the `observe` argument transform the store's state into the bare /// minimum of data needed for the feature to do its job in order to not hinder performance. /// This is especially true for root level features, and less important for leaf features. /// /// To read more about this performance technique, read the article. /// /// - Parameters: /// - store: A store. /// - toViewState: A transformation of `ViewState` to the state that will be observed for /// changes. /// - fromViewAction: A transformation of `ViewAction` that describes what actions can be sent. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed. public init( _ store: Store, observe toViewState: @escaping (_ state: State) -> ViewState, send fromViewAction: @escaping (_ viewAction: ViewAction) -> Action, removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool ) { #if DEBUG self.storeTypeName = ComposableArchitecture.storeTypeName(of: store) Logger.shared.log("View\(self.storeTypeName).init") #endif self.store = store._scope(state: toViewState, action: fromViewAction) self._state = CurrentValueRelay(self.store.withState { $0 }) self.viewCancellable = self.store.core.didSet .compactMap { [weak self] in self?.store.withState { $0 } } .removeDuplicates(by: isDuplicate) .dropFirst() .sink { [weak self] in self?.objectWillChange.send() self?._state.value = $0 } } init(_ viewStore: ViewStore) { #if DEBUG self.storeTypeName = viewStore.storeTypeName Logger.shared.log("View\(self.storeTypeName).init") #endif self.store = viewStore.store self._state = viewStore._state self.viewCancellable = viewStore.objectWillChange.sink { [weak self] in self?.objectWillChange.send() self?._state.value = viewStore.state } } #if DEBUG deinit { guard Thread.isMainThread else { return } MainActor._assumeIsolated { Logger.shared.log("View\(self.storeTypeName).deinit") } } #endif /// A publisher that emits when state changes. /// /// This publisher supports dynamic member lookup so that you can pluck out a specific field in /// the state: /// /// ```swift /// viewStore.publisher.alert /// .sink { ... } /// ``` /// /// When the emission happens the ``ViewStore``'s state has been updated, and so the following /// precondition will pass: /// /// ```swift /// viewStore.publisher /// .sink { precondition($0 == viewStore.state) } /// ``` /// /// This means you can either use the value passed to the closure or you can reach into /// `viewStore.state` directly. /// /// - Note: Due to a bug in Combine (or feature?), the order you `.sink` on a publisher has no /// bearing on the order the `.sink` closures are called. This means the work performed inside /// `viewStore.publisher.sink` closures should be completely independent of each other. Later /// closures cannot assume that earlier ones have already run. public var publisher: StorePublisher { StorePublisher(store: self, upstream: self._state) } /// The current state. public var state: ViewState { self._state.value } /// Returns the resulting value of a given key path. public subscript(dynamicMember keyPath: KeyPath) -> Value { self.state[keyPath: keyPath] } /// Sends an action to the store. /// /// This method returns a ``StoreTask``, which represents the lifecycle of the effect started /// from sending an action. You can use this value to tie the effect's lifecycle _and_ /// cancellation to an asynchronous context, such as SwiftUI's `task` view modifier: /// /// ```swift /// .task { await viewStore.send(.task).finish() } /// ``` /// /// > Important: ``ViewStore`` is not thread safe and you should only send actions to it from the /// > main thread. If you want to send actions on background threads due to the fact that the /// > reducer is performing computationally expensive work, then a better way to handle this is to /// > wrap that work in an ``Effect`` that is performed on a background thread so that the /// > result can be fed back into the store. /// /// - Parameter action: An action. /// - Returns: A ``StoreTask`` that represents the lifecycle of the effect executed when /// sending the action. @discardableResult public func send(_ action: ViewAction) -> StoreTask { self.store.send(action) } /// Sends an action to the store with a given animation. /// /// See ``ViewStore/send(_:)`` for more info. /// /// - Parameters: /// - action: An action. /// - animation: An animation. @discardableResult public func send(_ action: ViewAction, animation: Animation?) -> StoreTask { self.send(action, transaction: Transaction(animation: animation)) } /// Sends an action to the store with a given transaction. /// /// See ``ViewStore/send(_:)`` for more info. /// /// - Parameters: /// - action: An action. /// - transaction: A transaction. @discardableResult public func send(_ action: ViewAction, transaction: Transaction) -> StoreTask { withTransaction(transaction) { self.send(action) } } /// Sends an action into the store and then suspends while a piece of state is `true`. /// /// This method can be used to interact with async/await code, allowing you to suspend while work /// is being performed in an effect. One common example of this is using SwiftUI's `.refreshable` /// method, which shows a loading indicator on the screen while work is being performed. /// /// For example, suppose we wanted to load some data from the network when a pull-to-refresh /// gesture is performed on a list. The domain and logic for this feature can be modeled like so: /// /// ```swift /// @Reducer /// struct Feature { /// struct State: Equatable { /// var isLoading = false /// var response: String? /// } /// enum Action { /// case pulledToRefresh /// case receivedResponse(Result) /// } /// @Dependency(\.fetch) var fetch /// /// var body: some Reducer { /// Reduce { state, action in /// switch action { /// case .pulledToRefresh: /// state.isLoading = true /// return .run { send in /// await send(.receivedResponse(Result { try await self.fetch() })) /// } /// /// case let .receivedResponse(result): /// state.isLoading = false /// state.response = try? result.value /// return .none /// } /// } /// } /// } /// ``` /// /// Note that we keep track of an `isLoading` boolean in our state so that we know exactly when /// the network response is being performed. /// /// The view can show the fact in a `List`, if it's present, and we can use the `.refreshable` /// view modifier to enhance the list with pull-to-refresh capabilities: /// /// ```swift /// struct MyView: View { /// let store: Store /// /// var body: some View { /// WithViewStore(self.store, observe: { $0 }) { viewStore in /// List { /// if let response = viewStore.response { /// Text(response) /// } /// } /// .refreshable { /// await viewStore.send(.pulledToRefresh, while: \.isLoading) /// } /// } /// } /// } /// ``` /// /// Here we've used the ``send(_:while:)`` method to suspend while the `isLoading` state is /// `true`. Once that piece of state flips back to `false` the method will resume, signaling to /// `.refreshable` that the work has finished which will cause the loading indicator to disappear. /// /// - Parameters: /// - action: An action. /// - predicate: A predicate on `ViewState` that determines for how long this method should /// suspend. public func send( _ action: ViewAction, while predicate: @escaping (_ state: ViewState) -> Bool ) async { let task = self.send(action) await withTaskCancellationHandler { await self.yield(while: predicate) } onCancel: { task.cancel() } } /// Sends an action into the store and then suspends while a piece of state is `true`. /// /// See the documentation of ``send(_:while:)`` for more information. /// /// - Parameters: /// - action: An action. /// - animation: The animation to perform when the action is sent. /// - predicate: A predicate on `ViewState` that determines for how long this method should /// suspend. public func send( _ action: ViewAction, animation: Animation?, while predicate: @escaping (_ state: ViewState) -> Bool ) async { let task = withAnimation(animation) { self.send(action) } await withTaskCancellationHandler { await self.yield(while: predicate) } onCancel: { task.cancel() } } /// Suspends the current task while a predicate on state is `true`. /// /// If you want to suspend at the same time you send an action to the view store, use /// ``send(_:while:)``. /// /// - Parameter predicate: A predicate on `ViewState` that determines for how long this method /// should suspend. public func yield(while predicate: @escaping (_ state: ViewState) -> Bool) async { let isolatedCancellable = LockIsolated(nil) try? await withTaskCancellationHandler { try Task.checkCancellation() try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in guard !Task.isCancelled else { continuation.resume(throwing: CancellationError()) return } let cancellable = self.publisher .filter { !predicate($0) } .prefix(1) .sink { _ in continuation.resume() _ = isolatedCancellable } isolatedCancellable.setValue(cancellable) } } onCancel: { isolatedCancellable.value?.cancel() } } /// Derives a binding from the store that prevents direct writes to state and instead sends /// actions to the store. /// /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// since the ``Store`` does not allow directly writing its state; it only allows reading state /// and sending actions. /// /// For example, a text field binding can be created like this: /// /// ```swift /// struct State { var name = "" } /// enum Action { case nameChanged(String) } /// /// TextField( /// "Enter name", /// text: viewStore.binding( /// get: { $0.name }, /// send: { Action.nameChanged($0) } /// ) /// ) /// ``` /// /// - Parameters: /// - get: A function to get the state for the binding from the view store's full state. /// - valueToAction: A function that transforms the binding's value into an action that can be /// sent to the store. /// - Returns: A binding. public func binding( get: @escaping (_ state: ViewState) -> Value, send valueToAction: @escaping (_ value: Value) -> ViewAction ) -> Binding { ObservedObject(wrappedValue: self) .projectedValue[get: .init(rawValue: get), send: .init(rawValue: valueToAction)] } @_disfavoredOverload func binding( get: @escaping (_ state: ViewState) -> Value, compactSend valueToAction: @escaping (_ value: Value) -> ViewAction? ) -> Binding { ObservedObject(wrappedValue: self) .projectedValue[get: .init(rawValue: get), send: .init(rawValue: valueToAction)] } /// Derives a binding from the store that prevents direct writes to state and instead sends /// actions to the store. /// /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// since the ``Store`` does not allow directly writing its state; it only allows reading state /// and sending actions. /// /// For example, an alert binding can be dealt with like this: /// /// ```swift /// struct State { var alert: String? } /// enum Action { case alertDismissed } /// /// .alert( /// item: viewStore.binding( /// get: { $0.alert }, /// send: .alertDismissed /// ) /// ) { alert in Alert(title: Text(alert.message)) } /// ``` /// /// - Parameters: /// - get: A function to get the state for the binding from the view store's full state. /// - action: The action to send when the binding is written to. /// - Returns: A binding. public func binding( get: @escaping (_ state: ViewState) -> Value, send action: ViewAction ) -> Binding { self.binding(get: get, send: { _ in action }) } /// Derives a binding from the store that prevents direct writes to state and instead sends /// actions to the store. /// /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// since the ``Store`` does not allow directly writing its state; it only allows reading state /// and sending actions. /// /// For example, a text field binding can be created like this: /// /// ```swift /// typealias State = String /// enum Action { case nameChanged(String) } /// /// TextField( /// "Enter name", /// text: viewStore.binding( /// send: { Action.nameChanged($0) } /// ) /// ) /// ``` /// /// - Parameters: /// - valueToAction: A function that transforms the binding's value into an action that can be /// sent to the store. /// - Returns: A binding. public func binding( send valueToAction: @escaping (_ state: ViewState) -> ViewAction ) -> Binding { self.binding(get: { $0 }, send: valueToAction) } /// Derives a binding from the store that prevents direct writes to state and instead sends /// actions to the store. /// /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// since the ``Store`` does not allow directly writing its state; it only allows reading state /// and sending actions. /// /// For example, an alert binding can be dealt with like this: /// /// ```swift /// typealias State = String /// enum Action { case alertDismissed } /// /// .alert( /// item: viewStore.binding( /// send: .alertDismissed /// ) /// ) { title in Alert(title: Text(title)) } /// ``` /// /// - Parameters: /// - action: The action to send when the binding is written to. /// - Returns: A binding. public func binding(send action: ViewAction) -> Binding { self.binding(send: { _ in action }) } private subscript( get fromState: HashableWrapper<(ViewState) -> Value>, send toAction: HashableWrapper<(Value) -> ViewAction?> ) -> Value { get { fromState.rawValue(self.state) } set { BindingLocal.$isActive.withValue(true) { if let action = toAction.rawValue(newValue) { self.send(action) } } } } } /// A convenience type alias for referring to a view store of a given reducer's domain. /// /// Instead of specifying two generics: /// /// ```swift /// let viewStore: ViewStore /// ``` /// /// You can specify a single generic: /// /// ```swift /// let viewStore: ViewStoreOf /// ``` public typealias ViewStoreOf = ViewStore extension ViewStore where ViewState: Equatable { /// Initializes a view store from a store which observes changes to state. /// /// It is recommended that the `observe` argument transform the store's state into the bare /// minimum of data needed for the feature to do its job in order to not hinder performance. /// This is especially true for root level features, and less important for leaf features. /// /// To read more about this performance technique, read the article. /// /// - Parameters: /// - store: A store. /// - toViewState: A transformation of `ViewState` to the state that will be observed for /// changes. public convenience init( _ store: Store, observe toViewState: @escaping (_ state: State) -> ViewState ) { self.init(store, observe: toViewState, removeDuplicates: ==) } /// Initializes a view store from a store which observes changes to state. /// /// It is recommended that the `observe` argument transform the store's state into the bare /// minimum of data needed for the feature to do its job in order to not hinder performance. /// This is especially true for root level features, and less important for leaf features. /// /// To read more about this performance technique, read the article. /// /// - Parameters: /// - store: A store. /// - toViewState: A transformation of `ViewState` to the state that will be observed for /// changes. /// - fromViewAction: A transformation of `ViewAction` that describes what actions can be sent. public convenience init( _ store: Store, observe toViewState: @escaping (_ state: State) -> ViewState, send fromViewAction: @escaping (_ viewAction: ViewAction) -> Action ) { self.init(store, observe: toViewState, send: fromViewAction, removeDuplicates: ==) } } private struct HashableWrapper: Hashable { let rawValue: Value static func == (lhs: Self, rhs: Self) -> Bool { false } func hash(into hasher: inout Hasher) {} } enum BindingLocal { @TaskLocal static var isActive = false }