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 and 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 must /// return a view to be rendered: /// /// ```swift /// var body: some View { /// WithViewStore(self.store) { viewStore in /// VStack { /// Text("Current count: \(viewStore.count)") /// Button("Increment") { viewStore.send(.incrementButtonTapped) } /// } /// } /// } /// ``` /// /// 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 /// /// init(store: Store) { /// self.store = store /// self.viewStore = ViewStore(store) /// } /// /// 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) /// } /// ``` /// /// ### Thread safety /// /// The ``ViewStore`` class is not thread-safe, and all interactions with it must happen on the main /// thread. See the documentation of the ``Store`` class for more information why this decision was /// made. @dynamicMemberLookup public final class ViewStore: ObservableObject { /// A publisher of state. public let publisher: StorePublisher private var viewCancellable: AnyCancellable? // 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 private(set) lazy var objectWillChange = ObservableObjectPublisher() /// The current state. public var state: State { self.store.state.value } private let store: Store /// Initializes a view store from a store. /// /// - Parameters: /// - store: A store. /// - isDuplicate: A function to determine when two `State` values are equal. When values are /// equal, repeat view computations are removed. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool ) { self.publisher = StorePublisher(store.state, removeDuplicates: isDuplicate) self.store = store self.viewCancellable = store.state .dropFirst() .removeDuplicates(by: isDuplicate) .sink { [weak self] _ in self?.objectWillChange.send() } } /// Returns the resulting value of a given key path. public subscript(dynamicMember keyPath: KeyPath) -> LocalState { self.state[keyPath: keyPath] } /// Sends an action to the store. /// /// ``ViewStore`` is not thread safe and you should only send actions to it from the main thread. /// If you are wanting 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. public func send(_ action: Action) { self.store.send(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 /// 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. /// - localStateToViewAction: 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) -> LocalState, send localStateToViewAction: @escaping (LocalState) -> Action ) -> Binding { Binding( get: { get(self.state) }, set: { newLocalState, transaction in if transaction.animation != nil { withTransaction(transaction) { self.send(localStateToViewAction(newLocalState)) } } else { self.send(localStateToViewAction(newLocalState)) } } ) } /// 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: self.store.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) -> LocalState, send action: Action ) -> 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: /// - localStateToViewAction: 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 localStateToViewAction: @escaping (State) -> Action ) -> Binding { self.binding(get: { $0 }, send: localStateToViewAction) } /// 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: Action) -> Binding { self.binding(send: { _ in action }) } } extension ViewStore where State: Equatable { public convenience init(_ store: Store) { self.init(store, removeDuplicates: ==) } } extension ViewStore where State == Void { public convenience init(_ store: Store) { self.init(store, removeDuplicates: ==) } }