From 8a03e6aa109af4366087bda8fa9fc9ed110485b0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 11:11:39 -0700 Subject: [PATCH 01/26] wip --- Sources/ComposableArchitecture/Core.swift | 251 ++++++ Sources/ComposableArchitecture/Effect.swift | 6 +- .../Internal/CurrentValueRelay.swift | 2 +- .../IdentifiedArray+Observation.swift | 33 +- .../NavigationStack+Observation.swift | 62 +- .../Observation/Store+Observation.swift | 48 +- .../ComposableArchitecture/RootStore.swift | 154 ---- Sources/ComposableArchitecture/Store.swift | 163 ++-- .../SwiftUI/Alert.swift | 212 +++--- .../SwiftUI/Binding.swift | 25 +- .../SwiftUI/ConfirmationDialog.swift | 222 +++--- .../SwiftUI/Deprecated/ActionSheet.swift | 196 ++--- .../SwiftUI/Deprecated/LegacyAlert.swift | 174 ++--- .../Deprecated/NavigationLinkStore.swift | 376 ++++----- .../SwiftUI/ForEachStore.swift | 484 ++++++------ .../SwiftUI/FullScreenCover.swift | 214 +++--- .../SwiftUI/IfLetStore.swift | 612 +++++++-------- .../SwiftUI/NavigationDestination.swift | 260 +++---- .../SwiftUI/NavigationStackStore.swift | 381 ++++----- .../SwiftUI/Popover.swift | 232 +++--- .../SwiftUI/PresentationModifier.swift | 720 +++++++++--------- .../SwiftUI/Sheet.swift | 216 +++--- .../SwiftUI/SwitchStore.swift | 517 +++++++------ .../SwiftUI/WithViewStore.swift | 28 +- .../ComposableArchitecture/TestStore.swift | 12 +- .../UIKit/IfLetUIKit.swift | 25 +- .../NavigationStackControllerUIKit.swift | 55 +- .../ComposableArchitecture/ViewStore.swift | 9 +- 28 files changed, 2896 insertions(+), 2793 deletions(-) create mode 100644 Sources/ComposableArchitecture/Core.swift delete mode 100644 Sources/ComposableArchitecture/RootStore.swift diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift new file mode 100644 index 000000000000..9f7b1d14a91d --- /dev/null +++ b/Sources/ComposableArchitecture/Core.swift @@ -0,0 +1,251 @@ +import Combine +import Foundation + +@MainActor +protocol Core: AnyObject, Sendable { + associatedtype State + associatedtype Action + var state: State { get } + func send(_ action: Action) -> Task? + + var didSet: CurrentValueRelay { get } +} + +final class InvalidCore: Core { + var state: State { + get { fatalError() } + set { fatalError() } + } + func send(_ action: Action) -> Task? { nil } + + let didSet = CurrentValueRelay(()) +} + +final class RootCore: Core { + var state: Root.State { + didSet { + didSet.send(()) + } + } + let reducer: Root + var bufferedActions: [Root.Action] = [] + let didSet = CurrentValueRelay(()) + var effectCancellables: [UUID: AnyCancellable] = [:] + private var isSending = false + init( + initialState: Root.State, + reducer: Root + ) { + self.state = initialState + self.reducer = reducer + } + func send(_ action: Root.Action) -> Task? { + _withoutPerceptionChecking { + send(action, originatingFrom: nil) + } + } + func send(_ action: Root.Action, originatingFrom originatingAction: Any?) -> Task? { + self.bufferedActions.append(action) + guard !self.isSending else { return nil } + + self.isSending = true + var currentState = self.state + let tasks = LockIsolated<[Task]>([]) + defer { + withExtendedLifetime(self.bufferedActions) { + self.bufferedActions.removeAll() + } + self.state = currentState + self.isSending = false + if !self.bufferedActions.isEmpty { + if let task = self.send( + self.bufferedActions.removeLast(), + originatingFrom: originatingAction + ) { + tasks.withValue { $0.append(task) } + } + } + } + + var index = self.bufferedActions.startIndex + while index < self.bufferedActions.endIndex { + defer { index += 1 } + let action = self.bufferedActions[index] + let effect = reducer.reduce(into: ¤tState, action: action) + + switch effect.operation { + case .none: + break + case let .publisher(publisher): + var didComplete = false + let boxedTask = Box?>(wrappedValue: nil) + let uuid = UUID() + let effectCancellable = withEscapedDependencies { continuation in + publisher + .receive(on: UIScheduler.shared) + .handleEvents(receiveCancel: { [weak self] in self?.effectCancellables[uuid] = nil }) + .sink( + receiveCompletion: { [weak self] _ in + boxedTask.wrappedValue?.cancel() + didComplete = true + self?.effectCancellables[uuid] = nil + }, + receiveValue: { [weak self] effectAction in + guard let self else { return } + if let task = continuation.yield({ + self.send(effectAction, originatingFrom: action) + }) { + tasks.withValue { $0.append(task) } + } + } + ) + } + + if !didComplete { + let task = Task { @MainActor in + for await _ in AsyncStream.never {} + effectCancellable.cancel() + } + boxedTask.wrappedValue = task + tasks.withValue { $0.append(task) } + self.effectCancellables[uuid] = effectCancellable + } + case let .run(priority, operation): + withEscapedDependencies { continuation in + let task = Task(priority: priority) { @MainActor in + let isCompleted = LockIsolated(false) + defer { isCompleted.setValue(true) } + await operation( + Send { effectAction in + if isCompleted.value { + reportIssue( + """ + An action was sent from a completed effect: + + Action: + \(debugCaseOutput(effectAction)) + + Effect returned from: + \(debugCaseOutput(action)) + + Avoid sending actions using the 'send' argument from 'Effect.run' after \ + the effect has completed. This can happen if you escape the 'send' \ + argument in an unstructured context. + + To fix this, make sure that your 'run' closure does not return until \ + you're done calling 'send'. + """ + ) + } + if let task = continuation.yield({ + self.send(effectAction, originatingFrom: action) + }) { + tasks.withValue { $0.append(task) } + } + } + ) + } + tasks.withValue { $0.append(task) } + } + } + } + + guard !tasks.isEmpty else { return nil } + return Task { @MainActor in + await withTaskCancellationHandler { + var index = tasks.startIndex + while index < tasks.endIndex { + defer { index += 1 } + await tasks[index].value + } + } onCancel: { + var index = tasks.startIndex + while index < tasks.endIndex { + defer { index += 1 } + tasks[index].cancel() + } + } + } + } + private actor DefaultIsolation {} +} + +class ScopedCore: Core { + var base: Base + let stateKeyPath: KeyPath + let actionKeyPath: CaseKeyPath + init( + base: Base, + stateKeyPath: KeyPath, + actionKeyPath: CaseKeyPath + ) { + self.base = base + self.stateKeyPath = stateKeyPath + self.actionKeyPath = actionKeyPath + } + var state: State { + base.state[keyPath: stateKeyPath] + } + func send(_ action: Action) -> Task? { + base.send(actionKeyPath(action)) + } + var didSet: CurrentValueRelay { + base.didSet + } +} + +class IfLetCore: Core { + var base: Base + var cachedState: State + let stateKeyPath: KeyPath + let actionKeyPath: CaseKeyPath + init( + base: Base, + cachedState: State, + stateKeyPath: KeyPath, + actionKeyPath: CaseKeyPath + ) { + self.base = base + self.cachedState = cachedState + self.stateKeyPath = stateKeyPath + self.actionKeyPath = actionKeyPath + } + var state: State { + base.state[keyPath: stateKeyPath] ?? cachedState + } + func send(_ action: Action) -> Task? { + #if DEBUG + if BindingLocal.isActive && base.state[keyPath: stateKeyPath] == nil { + return nil + } + #endif + return base.send(actionKeyPath(action)) + } + var didSet: CurrentValueRelay { + base.didSet + } +} + +class ClosureScopedCore: Core { + var base: Base + let toState: (Base.State) -> State + let fromAction: (Action) -> Base.Action + init( + base: Base, + toState: @escaping (Base.State) -> State, + fromAction: @escaping (Action) -> Base.Action + ) { + self.base = base + self.toState = toState + self.fromAction = fromAction + } + var state: State { + toState(base.state) + } + func send(_ action: Action) -> Task? { + base.send(fromAction(action)) + } + var didSet: CurrentValueRelay { + base.didSet + } +} diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift index 4c5669705faf..c5d764a39cec 100644 --- a/Sources/ComposableArchitecture/Effect.swift +++ b/Sources/ComposableArchitecture/Effect.swift @@ -1,10 +1,10 @@ -import Combine +@preconcurrency import Combine import Foundation import SwiftUI -public struct Effect { +public struct Effect: Sendable { @usableFromInline - enum Operation { + enum Operation: Sendable { case none case publisher(AnyPublisher) case run(TaskPriority? = nil, @Sendable (_ send: Send) async -> Void) diff --git a/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift b/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift index 98e22980850c..2354b73ed160 100644 --- a/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift +++ b/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift @@ -1,7 +1,7 @@ import Combine import Foundation -final class CurrentValueRelay: Publisher { +final class CurrentValueRelay: Publisher, @unchecked Sendable { typealias Failure = Never private var currentValue: Output diff --git a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift index e668299337d5..5fcb47fbc048 100644 --- a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift @@ -119,17 +119,30 @@ public struct _StoreCollection: RandomAc else { return Store() } - let id = self.data.ids[position] - var element = self.data[position] - return self.store.scope( - id: self.store.id(state: \.[id:id]!, action: \.[id:id]), - state: ToState { - element = $0[id: id] ?? element - return element - }, - action: { .element(id: id, action: $0) }, - isInvalid: { !$0.ids.contains(id) } + let elementID = self.data.ids[position] + let scopeID = ScopeID, IdentifiedAction>( + state: \.[id:elementID], action: \.[id:elementID] ) + guard let child = self.store.children[scopeID] as? Store + else { + @MainActor + func open( + _ core: some Core, IdentifiedAction> + ) -> Store { + let child = Store( + core: IfLetCore( + base: core, + cachedState: self.data[position], + stateKeyPath: \.[id:elementID], + actionKeyPath: \.[id:elementID] + ) + ) + self.store.children[scopeID] = child + return child + } + return open(self.store.core) + } + return child } } } diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index 4c54f683f8db..7dd747dcc0e5 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -187,30 +187,50 @@ public struct _NavigationDestinationViewModifier< content .environment(\.navigationDestinationType, State.self) .navigationDestination(for: StackState.Component.self) { component in - var element = component.element - self - .destination( - self.store.scope( - id: self.store.id( - state: - \.[ - id:component.id,fileID:_HashableStaticString( - rawValue: fileID),filePath:_HashableStaticString( - rawValue: filePath),line:line,column:column - ], - action: \.[id:component.id] - ), - state: ToState { - element = $0[id: component.id] ?? element - return element - }, - action: { .element(id: component.id, action: $0) }, - isInvalid: { !$0.ids.contains(component.id) } - ) - ) + navigationDestination(component: component) .environment(\.navigationDestinationType, State.self) } } + + private func navigationDestination(component: StackState.Component) -> Destination { + let id = store.id( + state: + \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + action: \.[id:component.id] + ) + if let child = store.children[id] as? Store { + return destination(child) + } else { + @MainActor + func open( + _ core: some Core, StackAction> + ) -> Destination { + let child = Store( + core: IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) + ) + store.children[id] = child + return destination(child) + } + return open(store.core) + } + } } @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index 6b1ee73b2f91..bab58739a422 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -73,18 +73,22 @@ extension Store where State: ObservableState { /// > observed. /// /// - Parameters: - /// - state: A key path to optional child state. - /// - action: A case key path to child actions. + /// - stateKeyPath: A key path to optional child state. + /// - actionKeyPath: A case key path to child actions. + /// - fileID: The source `#fileID` associated with the scoping. + /// - filePath: The source `#filePath` associated with the scoping. + /// - line: The source `#line` associated with the scoping. + /// - column: The source `#column` associated with the scoping. /// - Returns: An optional store of non-optional child state and actions. public func scope( - state: KeyPath, - action: CaseKeyPath, + state stateKeyPath: KeyPath, + action actionKeyPath: CaseKeyPath, fileID: StaticString = #fileID, filePath: StaticString = #filePath, line: UInt = #line, column: UInt = #column ) -> Store? { - if !self.canCacheChildren { + if !canCacheChildren { reportIssue( uncachedStoreWarning(self), fileID: fileID, @@ -93,17 +97,29 @@ extension Store where State: ObservableState { column: column ) } - guard var childState = self.state[keyPath: state] - else { return nil } - return self.scope( - id: self.id(state: state.appending(path: \.!), action: action), - state: ToState { - childState = $0[keyPath: state] ?? childState - return childState - }, - action: { action($0) }, - isInvalid: { $0[keyPath: state] == nil } - ) + let id = ScopeID(state: stateKeyPath, action: actionKeyPath) + guard let childState = state[keyPath: stateKeyPath] + else { + children[id] = nil // TODO: Eager? + return nil + } + guard let child = children[id] as? Store + else { + func open(_ core: some Core) -> Store { + let child = Store( + core: IfLetCore( + base: core, + cachedState: childState, + stateKeyPath: stateKeyPath, + actionKeyPath: actionKeyPath + ) + ) + children[id] = child + return child + } + return open(core) + } + return child } } diff --git a/Sources/ComposableArchitecture/RootStore.swift b/Sources/ComposableArchitecture/RootStore.swift deleted file mode 100644 index 650ed7e812e7..000000000000 --- a/Sources/ComposableArchitecture/RootStore.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Combine -import Foundation - -@_spi(Internals) -@MainActor -public final class RootStore { - private var bufferedActions: [Any] = [] - let didSet = CurrentValueRelay(()) - @_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] = [:] - private var isSending = false - private let reducer: any Reducer - private(set) var state: Any { - didSet { - didSet.send(()) - } - } - - init( - initialState: State, - reducer: some Reducer - ) { - self.state = initialState - self.reducer = reducer - } - - func send(_ action: Any, originatingFrom originatingAction: Any? = nil) -> Task? { - func open(reducer: some Reducer) -> Task? { - self.bufferedActions.append(action) - guard !self.isSending else { return nil } - - self.isSending = true - var currentState = self.state as! State - let tasks = LockIsolated<[Task]>([]) - defer { - withExtendedLifetime(self.bufferedActions) { - self.bufferedActions.removeAll() - } - self.state = currentState - self.isSending = false - if !self.bufferedActions.isEmpty { - if let task = self.send( - self.bufferedActions.removeLast(), - originatingFrom: originatingAction - ) { - tasks.withValue { $0.append(task) } - } - } - } - - var index = self.bufferedActions.startIndex - while index < self.bufferedActions.endIndex { - defer { index += 1 } - let action = self.bufferedActions[index] as! Action - let effect = reducer.reduce(into: ¤tState, action: action) - - switch effect.operation { - case .none: - break - case let .publisher(publisher): - var didComplete = false - let boxedTask = Box?>(wrappedValue: nil) - let uuid = UUID() - let effectCancellable = withEscapedDependencies { continuation in - publisher - .receive(on: UIScheduler.shared) - .handleEvents(receiveCancel: { [weak self] in self?.effectCancellables[uuid] = nil }) - .sink( - receiveCompletion: { [weak self] _ in - boxedTask.wrappedValue?.cancel() - didComplete = true - self?.effectCancellables[uuid] = nil - }, - receiveValue: { [weak self] effectAction in - guard let self else { return } - if let task = continuation.yield({ - self.send(effectAction, originatingFrom: action) - }) { - tasks.withValue { $0.append(task) } - } - } - ) - } - - if !didComplete { - let task = Task { @MainActor in - for await _ in AsyncStream.never {} - effectCancellable.cancel() - } - boxedTask.wrappedValue = task - tasks.withValue { $0.append(task) } - self.effectCancellables[uuid] = effectCancellable - } - case let .run(priority, operation): - withEscapedDependencies { continuation in - let task = Task(priority: priority) { @MainActor in - let isCompleted = LockIsolated(false) - defer { isCompleted.setValue(true) } - await operation( - Send { effectAction in - if isCompleted.value { - reportIssue( - """ - An action was sent from a completed effect: - - Action: - \(debugCaseOutput(effectAction)) - - Effect returned from: - \(debugCaseOutput(action)) - - Avoid sending actions using the 'send' argument from 'Effect.run' after \ - the effect has completed. This can happen if you escape the 'send' \ - argument in an unstructured context. - - To fix this, make sure that your 'run' closure does not return until \ - you're done calling 'send'. - """ - ) - } - if let task = continuation.yield({ - self.send(effectAction, originatingFrom: action) - }) { - tasks.withValue { $0.append(task) } - } - } - ) - } - tasks.withValue { $0.append(task) } - } - } - } - - guard !tasks.isEmpty else { return nil } - return Task { @MainActor in - await withTaskCancellationHandler { - var index = tasks.startIndex - while index < tasks.endIndex { - defer { index += 1 } - await tasks[index].value - } - } onCancel: { - var index = tasks.startIndex - while index < tasks.endIndex { - defer { index += 1 } - tasks[index].cancel() - } - } - } - } - return _withoutPerceptionChecking { - open(reducer: self.reducer) - } - } -} diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 8527f27e117b..0ef110d8a43a 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -1,4 +1,4 @@ -import Combine +@preconcurrency import Combine import Foundation import SwiftUI @@ -139,12 +139,10 @@ import SwiftUI #endif public final class Store { var canCacheChildren = true - private var children: [ScopeID: AnyObject] = [:] + var children: [ScopeID: AnyObject] = [:] var _isInvalidated: @MainActor @Sendable () -> Bool = { false } - @_spi(Internals) public let rootStore: RootStore - private let toState: PartialToState - private let fromAction: (Action) -> Any + let core: any Core #if !os(visionOS) let _$observationRegistrar = PerceptionRegistrar( @@ -179,9 +177,7 @@ public final class Store { init() { self._isInvalidated = { true } - self.rootStore = RootStore(initialState: (), reducer: EmptyReducer()) - self.toState = .keyPath(\State.self) - self.fromAction = { $0 } + self.core = InvalidCore() } deinit { @@ -227,7 +223,7 @@ public final class Store { /// sending the action. @discardableResult public func send(_ action: Action) -> StoreTask { - .init(rawValue: self.send(action, originatingFrom: nil)) + .init(rawValue: self.send(action)) } /// Sends an action to the store with a given animation. @@ -252,7 +248,7 @@ public final class Store { @discardableResult public func send(_ action: Action, transaction: Transaction) -> StoreTask { withTransaction(transaction) { - .init(rawValue: self.send(action, originatingFrom: nil)) + .init(rawValue: self.send(action)) } } @@ -300,12 +296,18 @@ public final class Store { state: KeyPath, action: CaseKeyPath ) -> Store { - self.scope( - id: self.id(state: state, action: action), - state: ToState(state), - action: { action($0) }, - isInvalid: nil - ) + let id = ScopeID(state: state, action: action) + guard let child = children[id] as? Store else { + func open(_ core: some Core) -> Store { + let child = Store( + core: ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) + ) + children[id] = child + return child + } + return open(core) + } + return child } @available( @@ -317,110 +319,63 @@ public final class Store { state toChildState: @escaping (_ state: State) -> ChildState, action fromChildAction: @escaping (_ childAction: ChildAction) -> Action ) -> Store { - self.scope( - id: nil, - state: ToState(toChildState), - action: fromChildAction, - isInvalid: nil - ) + _scope(state: toChildState, action: fromChildAction) } - @_spi(Internals) - public var currentState: State { - self.toState(self.rootStore.state) + func _scope( + state toChildState: @escaping (_ state: State) -> ChildState, + action fromChildAction: @escaping (_ childAction: ChildAction) -> Action + ) -> Store { + func open(_ core: some Core) -> Store { + Store( + core: ClosureScopedCore( + base: core, + toState: toChildState, + fromAction: fromChildAction + ) + ) + } + return open(core) } @_spi(Internals) - public - func scope( - id: ScopeID?, - state: ToState, - action fromChildAction: @escaping (ChildAction) -> Action, - isInvalid: ((State) -> Bool)? - ) -> Store - { - if self.canCacheChildren, - let id = id, - let childStore = self.children[id] as? Store - { - return childStore - } - let childStore = Store( - rootStore: self.rootStore, - toState: self.toState.appending(state.base), - fromAction: { [fromAction] in fromAction(fromChildAction($0)) } - ) - childStore._isInvalidated = - id == nil || !self.canCacheChildren - ? { @MainActor @Sendable in - isInvalid?(self.currentState) == true || self._isInvalidated() - } - : { @MainActor @Sendable [weak self] in - guard let self else { return true } - return isInvalid?(self.currentState) == true || self._isInvalidated() - } - childStore.canCacheChildren = self.canCacheChildren && id != nil - if let id = id, self.canCacheChildren { - self.children[id] = childStore - } - return childStore + public var currentState: State { + core.state } @_spi(Internals) - public func send( - _ action: Action, - originatingFrom originatingAction: Action? - ) -> Task? { - #if DEBUG - if BindingLocal.isActive && self._isInvalidated() { - return .none - } - #endif - return self.rootStore.send(self.fromAction(action)) + @_disfavoredOverload + public func send(_ action: Action) -> Task? { + core.send(action) } - private init( - rootStore: RootStore, - toState: PartialToState, - fromAction: @escaping (Action) -> Any - ) { + init(core: some Core) { defer { Logger.shared.log("\(storeTypeName(of: self)).init") } - self.rootStore = rootStore - self.toState = toState - self.fromAction = fromAction - - func subscribeToDidSet(_ type: T.Type) -> AnyCancellable { - let toState = toState as! PartialToState - return rootStore.didSet - .compactMap { [weak rootStore] in - rootStore.map { toState($0.state) }?._$id - } - .removeDuplicates() - .dropFirst() - .sink { [weak self] _ in + self.core = core + let didSet = core.didSet + + if State.self is any ObservableState.Type { + @Sendable + func onChange() { + withPerceptionTracking { + MainActor.assumeIsolated { _ = core.state } + } onChange: { [weak self] in guard let self else { return } - self._$observationRegistrar.withMutation(of: self, keyPath: \.currentState) {} + MainActor.assumeIsolated { + self._$observationRegistrar.withMutation(of: self, keyPath: \.currentState) {} + } + didSet.send(()) + onChange() } - } - - if let stateType = State.self as? any ObservableState.Type { - self.parentCancellable = subscribeToDidSet(stateType) + } } } - - convenience init( + + convenience init>( initialState: R.State, reducer: R - ) - where - R.State == State, - R.Action == Action - { - self.init( - rootStore: RootStore(initialState: initialState, reducer: reducer), - toState: .keyPath(\State.self), - fromAction: { $0 } - ) + ) { + self.init(core: RootCore(initialState: initialState, reducer: reducer)) } /// A publisher that emits when state changes. @@ -435,7 +390,7 @@ public final class Store { public var publisher: StorePublisher { StorePublisher( store: self, - upstream: self.rootStore.didSet.map { self.currentState } + upstream: self.core.didSet.map { self.currentState } ) } diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 80b19f63cd96..83cd68c85084 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -1,106 +1,106 @@ -import SwiftUI - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension View { - /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func alert( - store: Store>, PresentationAction> - ) -> some View { - self._alert(store: store, state: { $0 }, action: { $0 }) - } - - /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - /// - toDestinationState: A transformation to extract alert state from the presentation state. - /// - fromDestinationAction: A transformation to embed alert actions into the presentation - /// action. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func alert( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> AlertState?, - action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action - ) -> some View { - self._alert(store: store, state: toDestinationState, action: fromDestinationAction) - } - - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - private func _alert( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> AlertState?, - action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $isPresented, destination in - let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } - self.alert( - (alertState?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: $isPresented, - presenting: alertState, - actions: { alertState in - ForEach(alertState.buttons) { button in - Button(role: button.role.map(ButtonRole.init)) { - switch button.action.type { - case let .send(action): - if let action { - store.send(.presented(fromDestinationAction(action))) - } - case let .animatedSend(action, animation): - if let action { - store.send(.presented(fromDestinationAction(action)), animation: animation) - } - } - } label: { - Text(button.label) - } - } - }, - message: { - $0.message.map(Text.init) - } - ) - } - } -} +//import SwiftUI +// +//@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +//extension View { +// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes +// /// `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func alert( +// store: Store>, PresentationAction> +// ) -> some View { +// self._alert(store: store, state: { $0 }, action: { $0 }) +// } +// +// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes +// /// `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// /// - toDestinationState: A transformation to extract alert state from the presentation state. +// /// - fromDestinationAction: A transformation to embed alert actions into the presentation +// /// action. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func alert( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> AlertState?, +// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action +// ) -> some View { +// self._alert(store: store, state: toDestinationState, action: fromDestinationAction) +// } +// +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// private func _alert( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> AlertState?, +// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $isPresented, destination in +// let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } +// self.alert( +// (alertState?.title).map(Text.init) ?? Text(verbatim: ""), +// isPresented: $isPresented, +// presenting: alertState, +// actions: { alertState in +// ForEach(alertState.buttons) { button in +// Button(role: button.role.map(ButtonRole.init)) { +// switch button.action.type { +// case let .send(action): +// if let action { +// store.send(.presented(fromDestinationAction(action))) +// } +// case let .animatedSend(action, animation): +// if let action { +// store.send(.presented(fromDestinationAction(action)), animation: animation) +// } +// } +// } label: { +// Text(button.label) +// } +// } +// }, +// message: { +// $0.message.map(Text.init) +// } +// ) +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 5d19a605edc2..47e5e0c8f1f4 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -421,12 +421,7 @@ public struct BindingViewStore { line: UInt = #line, column: UInt = #column ) { - self.store = store.scope( - id: nil, - state: ToState(\.self), - action: Action.binding, - isInvalid: nil - ) + self.store = store._scope(state: { $0 }, action: { .binding($0) }) #if DEBUG self.bindableActionType = type(of: Action.self) self.fileID = fileID @@ -511,12 +506,7 @@ extension ViewStore { observe: { (_: State) in toViewState( BindingViewStore( - store: store.scope( - id: nil, - state: ToState(\.self), - action: fromViewAction, - isInvalid: nil - ) + store: store._scope(state: { $0 }, action: fromViewAction) ) ) }, @@ -626,16 +616,7 @@ extension WithViewStore where Content: View { self.init( store, observe: { (_: State) in - toViewState( - BindingViewStore( - store: store.scope( - id: nil, - state: ToState(\.self), - action: fromViewAction, - isInvalid: nil - ) - ) - ) + toViewState(BindingViewStore(store: store._scope(state: { $0 }, action: fromViewAction))) }, send: fromViewAction, removeDuplicates: isDuplicate, diff --git a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift index 5f325fe746c7..160898d4ec99 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift @@ -1,111 +1,111 @@ -import SwiftUI - -@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -extension View { - /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a - /// dialog. - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func confirmationDialog( - store: Store< - PresentationState>, - PresentationAction - > - ) -> some View { - self._confirmationDialog(store: store, state: { $0 }, action: { $0 }) - } - - /// Displays a dialog when then store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a - /// dialog. - /// - toDestinationState: A transformation to extract dialog state from the presentation state. - /// - fromDestinationAction: A transformation to embed dialog actions into the presentation - /// action. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func confirmationDialog( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, - action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action - ) -> some View { - self._confirmationDialog(store: store, state: toDestinationState, action: fromDestinationAction) - } - - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - private func _confirmationDialog( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, - action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $isPresented, destination in - let confirmationDialogState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } - self.confirmationDialog( - (confirmationDialogState?.title).map(Text.init) ?? Text(verbatim: ""), - isPresented: $isPresented, - titleVisibility: (confirmationDialogState?.titleVisibility).map(Visibility.init) - ?? .automatic, - presenting: confirmationDialogState, - actions: { confirmationDialogState in - ForEach(confirmationDialogState.buttons) { button in - Button(role: button.role.map(ButtonRole.init)) { - switch button.action.type { - case let .send(action): - if let action { - store.send(.presented(fromDestinationAction(action))) - } - case let .animatedSend(action, animation): - if let action { - store.send(.presented(fromDestinationAction(action)), animation: animation) - } - } - } label: { - Text(button.label) - } - } - }, - message: { - $0.message.map(Text.init) - } - ) - } - } -} +//import SwiftUI +// +//@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +//extension View { +// /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes +// /// `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a +// /// dialog. +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func confirmationDialog( +// store: Store< +// PresentationState>, +// PresentationAction +// > +// ) -> some View { +// self._confirmationDialog(store: store, state: { $0 }, action: { $0 }) +// } +// +// /// Displays a dialog when then store's state becomes non-`nil`, and dismisses it when it becomes +// /// `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a +// /// dialog. +// /// - toDestinationState: A transformation to extract dialog state from the presentation state. +// /// - fromDestinationAction: A transformation to embed dialog actions into the presentation +// /// action. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func confirmationDialog( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, +// action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action +// ) -> some View { +// self._confirmationDialog(store: store, state: toDestinationState, action: fromDestinationAction) +// } +// +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// private func _confirmationDialog( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, +// action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $isPresented, destination in +// let confirmationDialogState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } +// self.confirmationDialog( +// (confirmationDialogState?.title).map(Text.init) ?? Text(verbatim: ""), +// isPresented: $isPresented, +// titleVisibility: (confirmationDialogState?.titleVisibility).map(Visibility.init) +// ?? .automatic, +// presenting: confirmationDialogState, +// actions: { confirmationDialogState in +// ForEach(confirmationDialogState.buttons) { button in +// Button(role: button.role.map(ButtonRole.init)) { +// switch button.action.type { +// case let .send(action): +// if let action { +// store.send(.presented(fromDestinationAction(action))) +// } +// case let .animatedSend(action, animation): +// if let action { +// store.send(.presented(fromDestinationAction(action)), animation: animation) +// } +// } +// } label: { +// Text(button.label) +// } +// } +// }, +// message: { +// $0.message.map(Text.init) +// } +// ) +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift index bacb3608bed8..978fb00bdfbb 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift @@ -1,98 +1,98 @@ -import SwiftUI - -extension View { - /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it - /// becomes `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - /// - toDestinationState: A transformation to extract alert state from the presentation state. - /// - fromDestinationAction: A transformation to embed alert actions into the presentation - /// action. - @available( - iOS, - introduced: 13, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:)' instead." - ) - @available(macOS, unavailable) - @available( - tvOS, - introduced: 13, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:)' instead." - ) - @available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:)' instead." - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func actionSheet( - store: Store< - PresentationState>, PresentationAction - > - ) -> some View { - self.actionSheet(store: store, state: { $0 }, action: { $0 }) - } - - /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes - /// `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - /// - toDestinationState: A transformation to extract alert state from the presentation state. - /// - fromDestinationAction: A transformation to embed alert actions into the presentation - /// action. - @available( - iOS, - introduced: 13, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:state:action:)' instead." - ) - @available(macOS, unavailable) - @available( - tvOS, - introduced: 13, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:state:action:)' instead." - ) - @available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "use 'View.confirmationDialog(store:state:action:)' instead." - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func actionSheet( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, - action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, _ in - let actionSheetState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } - self.actionSheet(item: $item) { _ in - ActionSheet(actionSheetState!) { action in - if let action { - store.send(.presented(fromDestinationAction(action))) - } else { - store.send(.dismiss) - } - } - } - } - } -} +//import SwiftUI +// +//extension View { +// /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it +// /// becomes `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// /// - toDestinationState: A transformation to extract alert state from the presentation state. +// /// - fromDestinationAction: A transformation to embed alert actions into the presentation +// /// action. +// @available( +// iOS, +// introduced: 13, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:)' instead." +// ) +// @available(macOS, unavailable) +// @available( +// tvOS, +// introduced: 13, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:)' instead." +// ) +// @available( +// watchOS, +// introduced: 6, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:)' instead." +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func actionSheet( +// store: Store< +// PresentationState>, PresentationAction +// > +// ) -> some View { +// self.actionSheet(store: store, state: { $0 }, action: { $0 }) +// } +// +// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes +// /// `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// /// - toDestinationState: A transformation to extract alert state from the presentation state. +// /// - fromDestinationAction: A transformation to embed alert actions into the presentation +// /// action. +// @available( +// iOS, +// introduced: 13, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:state:action:)' instead." +// ) +// @available(macOS, unavailable) +// @available( +// tvOS, +// introduced: 13, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:state:action:)' instead." +// ) +// @available( +// watchOS, +// introduced: 6, +// deprecated: 100000, +// message: "use 'View.confirmationDialog(store:state:action:)' instead." +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func actionSheet( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, +// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, _ in +// let actionSheetState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } +// self.actionSheet(item: $item) { _ in +// ActionSheet(actionSheetState!) { action in +// if let action { +// store.send(.presented(fromDestinationAction(action))) +// } else { +// store.send(.dismiss) +// } +// } +// } +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift index 21ee708eb5f8..76f3bfa0650b 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift @@ -1,87 +1,87 @@ -import SwiftUI - -extension View { - /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it - /// becomes `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - @available(iOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") - @available( - macOS, introduced: 10.15, deprecated: 100000, message: "use `View.alert(store:) instead." - ) - @available(tvOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") - @available( - watchOS, introduced: 6, deprecated: 100000, message: "use `View.alert(store:) instead." - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func legacyAlert( - store: Store>, PresentationAction> - ) -> some View { - self.legacyAlert(store: store, state: { $0 }, action: { $0 }) - } - - /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it - /// becomes `nil`. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an - /// alert. - /// - toDestinationState: A transformation to extract alert state from the presentation state. - /// - fromDestinationAction: A transformation to embed alert actions into the presentation - /// action. - @available( - iOS, - introduced: 13, - deprecated: 100000, - message: "use `View.alert(store:state:action:) instead." - ) - @available( - macOS, - introduced: 10.15, - deprecated: 100000, - message: "use `View.alert(store:state:action:) instead." - ) - @available( - tvOS, - introduced: 13, - deprecated: 100000, - message: "use `View.alert(store:state:action:) instead." - ) - @available( - watchOS, - introduced: 6, - deprecated: 100000, - message: "use `View.alert(store:state:action:) instead." - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func legacyAlert( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> AlertState?, - action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, _ in - let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } - self.alert(item: $item) { _ in - Alert(alertState!) { action in - if let action { - store.send(.presented(fromDestinationAction(action))) - } else { - store.send(.dismiss) - } - } - } - } - } -} +//import SwiftUI +// +//extension View { +// /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it +// /// becomes `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// @available(iOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") +// @available( +// macOS, introduced: 10.15, deprecated: 100000, message: "use `View.alert(store:) instead." +// ) +// @available(tvOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") +// @available( +// watchOS, introduced: 6, deprecated: 100000, message: "use `View.alert(store:) instead." +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func legacyAlert( +// store: Store>, PresentationAction> +// ) -> some View { +// self.legacyAlert(store: store, state: { $0 }, action: { $0 }) +// } +// +// /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it +// /// becomes `nil`. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an +// /// alert. +// /// - toDestinationState: A transformation to extract alert state from the presentation state. +// /// - fromDestinationAction: A transformation to embed alert actions into the presentation +// /// action. +// @available( +// iOS, +// introduced: 13, +// deprecated: 100000, +// message: "use `View.alert(store:state:action:) instead." +// ) +// @available( +// macOS, +// introduced: 10.15, +// deprecated: 100000, +// message: "use `View.alert(store:state:action:) instead." +// ) +// @available( +// tvOS, +// introduced: 13, +// deprecated: 100000, +// message: "use `View.alert(store:state:action:) instead." +// ) +// @available( +// watchOS, +// introduced: 6, +// deprecated: 100000, +// message: "use `View.alert(store:state:action:) instead." +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func legacyAlert( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> AlertState?, +// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, _ in +// let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } +// self.alert(item: $item) { _ in +// Alert(alertState!) { action in +// if let action { +// store.send(.presented(fromDestinationAction(action))) +// } else { +// store.send(.dismiss) +// } +// } +// } +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift index 9a59c73f08cf..8de993e7e722 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift @@ -1,188 +1,188 @@ -import SwiftUI - -/// A view that controls a navigation presentation. -/// -/// This view is similar to SwiftUI's `NavigationLink`, but it allows driving navigation from an -/// optional or enum instead of just a boolean. -/// -/// Typically you use this view by first modeling your features as having a parent feature that -/// holds onto an optional piece of child state using the ``PresentationState``, -/// ``PresentationAction`` and ``Reducer/ifLet(_:action:destination:fileID:filePath:line:column:)-4ub6q`` tools (see -/// for more information). Then in the view you can construct a -/// `NavigationLinkStore` by passing a ``Store`` that is focused on the presentation domain: -/// -/// ```swift -/// NavigationLinkStore( -/// self.store.scope(state: \.$child, action: \.child) -/// ) { -/// viewStore.send(.linkTapped) -/// } destination: { store in -/// ChildView(store: store) -/// } label: { -/// Text("Go to child") -/// } -/// ``` -/// -/// Then when the `child` state flips from `nil` to non-`nil` a drill-down animation will occur to -/// the child domain. -@available(iOS, introduced: 13, deprecated: 16) -@available(macOS, introduced: 10.15, deprecated: 13) -@available(tvOS, introduced: 13, deprecated: 16) -@available(watchOS, introduced: 6, deprecated: 9) -public struct NavigationLinkStore< - State, - Action, - DestinationState, - DestinationAction, - Destination: View, - Label: View ->: View { - let store: Store, PresentationAction> - @ObservedObject var viewStore: ViewStore> - let toDestinationState: (State) -> DestinationState? - let fromDestinationAction: (DestinationAction) -> Action - let onTap: () -> Void - let destination: (Store) -> Destination - let label: Label - var isDetailLink = true - - public init( - _ store: Store, PresentationAction>, - onTap: @escaping () -> Void, - @ViewBuilder destination: @escaping (_ store: Store) -> Destination, - @ViewBuilder label: () -> Label - ) where State == DestinationState, Action == DestinationAction { - self.init( - store, - state: { $0 }, - action: { $0 }, - onTap: onTap, - destination: destination, - label: label - ) - } - - public init( - _ store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - onTap: @escaping () -> Void, - @ViewBuilder destination: @escaping (_ store: Store) -> - Destination, - @ViewBuilder label: () -> Label - ) { - let store = store.scope( - id: nil, - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } - ) - self.store = store - self.viewStore = ViewStore( - store.scope( - id: nil, - state: ToState { $0.wrappedValue.flatMap(toDestinationState) != nil }, - action: { $0 }, - isInvalid: nil - ), - observe: { $0 } - ) - self.toDestinationState = toDestinationState - self.fromDestinationAction = fromDestinationAction - self.onTap = onTap - self.destination = destination - self.label = label() - } - - public init( - _ store: Store, PresentationAction>, - id: State.ID, - onTap: @escaping () -> Void, - @ViewBuilder destination: @escaping (_ store: Store) -> Destination, - @ViewBuilder label: () -> Label - ) where State == DestinationState, Action == DestinationAction, State: Identifiable { - self.init( - store, - state: { $0 }, - action: { $0 }, - id: id, - onTap: onTap, - destination: destination, - label: label - ) - } - - public init( - _ store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - id: DestinationState.ID, - onTap: @escaping () -> Void, - @ViewBuilder destination: @escaping (_ store: Store) -> - Destination, - @ViewBuilder label: () -> Label - ) where DestinationState: Identifiable { - let store = store.scope( - id: nil, - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0.wrappedValue.flatMap(toDestinationState)?.id != id } - ) - self.store = store - self.viewStore = ViewStore( - store.scope( - id: nil, - state: ToState { $0.wrappedValue.flatMap(toDestinationState)?.id == id }, - action: { $0 }, - isInvalid: nil - ), - observe: { $0 } - ) - self.toDestinationState = toDestinationState - self.fromDestinationAction = fromDestinationAction - self.onTap = onTap - self.destination = destination - self.label = label() - } - - public var body: some View { - NavigationLink( - isActive: Binding( - get: { self.viewStore.state }, - set: { - if $0 { - withTransaction($1, self.onTap) - } else if self.viewStore.state { - self.viewStore.send(.dismiss, transaction: $1) - } - } - ) - ) { - IfLetStore( - self.store.scope( - id: nil, - state: ToState( - returningLastNonNilValue { $0.wrappedValue.flatMap(self.toDestinationState) } - ), - action: { .presented(self.fromDestinationAction($0)) }, - isInvalid: nil - ), - then: self.destination - ) - } label: { - self.label - } - #if os(iOS) - .isDetailLink(self.isDetailLink) - #endif - } - - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - public func isDetailLink(_ isDetailLink: Bool) -> Self { - var link = self - link.isDetailLink = isDetailLink - return link - } -} +//import SwiftUI +// +///// A view that controls a navigation presentation. +///// +///// This view is similar to SwiftUI's `NavigationLink`, but it allows driving navigation from an +///// optional or enum instead of just a boolean. +///// +///// Typically you use this view by first modeling your features as having a parent feature that +///// holds onto an optional piece of child state using the ``PresentationState``, +///// ``PresentationAction`` and ``Reducer/ifLet(_:action:destination:fileID:filePath:line:column:)-4ub6q`` tools (see +///// for more information). Then in the view you can construct a +///// `NavigationLinkStore` by passing a ``Store`` that is focused on the presentation domain: +///// +///// ```swift +///// NavigationLinkStore( +///// self.store.scope(state: \.$child, action: \.child) +///// ) { +///// viewStore.send(.linkTapped) +///// } destination: { store in +///// ChildView(store: store) +///// } label: { +///// Text("Go to child") +///// } +///// ``` +///// +///// Then when the `child` state flips from `nil` to non-`nil` a drill-down animation will occur to +///// the child domain. +//@available(iOS, introduced: 13, deprecated: 16) +//@available(macOS, introduced: 10.15, deprecated: 13) +//@available(tvOS, introduced: 13, deprecated: 16) +//@available(watchOS, introduced: 6, deprecated: 9) +//public struct NavigationLinkStore< +// State, +// Action, +// DestinationState, +// DestinationAction, +// Destination: View, +// Label: View +//>: View { +// let store: Store, PresentationAction> +// @ObservedObject var viewStore: ViewStore> +// let toDestinationState: (State) -> DestinationState? +// let fromDestinationAction: (DestinationAction) -> Action +// let onTap: () -> Void +// let destination: (Store) -> Destination +// let label: Label +// var isDetailLink = true +// +// public init( +// _ store: Store, PresentationAction>, +// onTap: @escaping () -> Void, +// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, +// @ViewBuilder label: () -> Label +// ) where State == DestinationState, Action == DestinationAction { +// self.init( +// store, +// state: { $0 }, +// action: { $0 }, +// onTap: onTap, +// destination: destination, +// label: label +// ) +// } +// +// public init( +// _ store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// onTap: @escaping () -> Void, +// @ViewBuilder destination: @escaping (_ store: Store) -> +// Destination, +// @ViewBuilder label: () -> Label +// ) { +// let store = store.scope( +// id: nil, +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } +// ) +// self.store = store +// self.viewStore = ViewStore( +// store.scope( +// id: nil, +// state: ToState { $0.wrappedValue.flatMap(toDestinationState) != nil }, +// action: { $0 }, +// isInvalid: nil +// ), +// observe: { $0 } +// ) +// self.toDestinationState = toDestinationState +// self.fromDestinationAction = fromDestinationAction +// self.onTap = onTap +// self.destination = destination +// self.label = label() +// } +// +// public init( +// _ store: Store, PresentationAction>, +// id: State.ID, +// onTap: @escaping () -> Void, +// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, +// @ViewBuilder label: () -> Label +// ) where State == DestinationState, Action == DestinationAction, State: Identifiable { +// self.init( +// store, +// state: { $0 }, +// action: { $0 }, +// id: id, +// onTap: onTap, +// destination: destination, +// label: label +// ) +// } +// +// public init( +// _ store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// id: DestinationState.ID, +// onTap: @escaping () -> Void, +// @ViewBuilder destination: @escaping (_ store: Store) -> +// Destination, +// @ViewBuilder label: () -> Label +// ) where DestinationState: Identifiable { +// let store = store.scope( +// id: nil, +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0.wrappedValue.flatMap(toDestinationState)?.id != id } +// ) +// self.store = store +// self.viewStore = ViewStore( +// store.scope( +// id: nil, +// state: ToState { $0.wrappedValue.flatMap(toDestinationState)?.id == id }, +// action: { $0 }, +// isInvalid: nil +// ), +// observe: { $0 } +// ) +// self.toDestinationState = toDestinationState +// self.fromDestinationAction = fromDestinationAction +// self.onTap = onTap +// self.destination = destination +// self.label = label() +// } +// +// public var body: some View { +// NavigationLink( +// isActive: Binding( +// get: { self.viewStore.state }, +// set: { +// if $0 { +// withTransaction($1, self.onTap) +// } else if self.viewStore.state { +// self.viewStore.send(.dismiss, transaction: $1) +// } +// } +// ) +// ) { +// IfLetStore( +// self.store.scope( +// id: nil, +// state: ToState( +// returningLastNonNilValue { $0.wrappedValue.flatMap(self.toDestinationState) } +// ), +// action: { .presented(self.fromDestinationAction($0)) }, +// isInvalid: nil +// ), +// then: self.destination +// ) +// } label: { +// self.label +// } +// #if os(iOS) +// .isDetailLink(self.isDetailLink) +// #endif +// } +// +// @available(macOS, unavailable) +// @available(tvOS, unavailable) +// @available(watchOS, unavailable) +// public func isDetailLink(_ isDetailLink: Bool) -> Self { +// var link = self +// link.isDetailLink = isDetailLink +// return link +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift index 1b2cb2d02386..641b6c509444 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift @@ -1,242 +1,242 @@ -import OrderedCollections -import SwiftUI - -/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with -/// collections of state. -/// -/// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each -/// element. This allows you to extract and modularize an element's view and avoid concerns around -/// collection index math and parent-child store communication. -/// -/// For example, a todos app may define the domain and logic associated with an individual todo: -/// -/// ```swift -/// @Reducer -/// struct Todo { -/// struct State: Equatable, Identifiable { -/// let id: UUID -/// var description = "" -/// var isComplete = false -/// } -/// -/// enum Action { -/// case isCompleteToggled(Bool) -/// case descriptionChanged(String) -/// } -/// -/// var body: some Reducer { -/// // ... -/// } -/// } -/// ``` -/// -/// As well as a view with a domain-specific store: -/// -/// ```swift -/// struct TodoView: View { -/// let store: StoreOf -/// var body: some View { /* ... */ } -/// } -/// ``` -/// -/// For a parent domain to work with a collection of todos, it can hold onto this collection in -/// state: -/// -/// ```swift -/// @Reducer -/// struct Todos { -/// struct State: Equatable { -/// var todos: IdentifiedArrayOf = [] -/// } -/// // ... -/// } -/// ``` -/// -/// Define a case to handle actions sent to the child domain: -/// -/// ```swift -/// enum Action { -/// case todos(IdentifiedActionOf) -/// } -/// ``` -/// -/// Enhance its core reducer using -/// ``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-3dw7i``: -/// -/// ```swift -/// var body: some Reducer { -/// Reduce { state, action in -/// // ... -/// } -/// .forEach(\.todos, action: \.todos) { -/// Todo() -/// } -/// } -/// ``` -/// -/// And finally render a list of `TodoView`s using ``ForEachStore``: -/// -/// ```swift -/// ForEachStore( -/// self.store.scope(state: \.todos, action: \.todos) -/// ) { todoStore in -/// TodoView(store: todoStore) -/// } -/// ``` -/// -@available( - iOS, deprecated: 9999, - message: - "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -) -@available( - macOS, deprecated: 9999, - message: - "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -) -@available( - tvOS, deprecated: 9999, - message: - "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -) -@available( - watchOS, deprecated: 9999, - message: - "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -) -public struct ForEachStore< - EachState, EachAction, Data: Collection, ID: Hashable & Sendable, Content: View ->: View { - public let data: Data - let content: Content - - /// Initializes a structure that computes views on demand from a store on a collection of data and - /// an identified action. - /// - /// - Parameters: - /// - store: A store on an identified array of data and an identified action. - /// - content: A function that can generate content given a store of an element. - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, IdentifiedAction>, - @ViewBuilder content: @escaping (_ store: Store) -> EachContent - ) - where - Data == IdentifiedArray, - Content == WithViewStore< - IdentifiedArray, IdentifiedAction, - ForEach, ID, EachContent> - > - { - self.data = store.withState { $0 } - self.content = WithViewStore( - store, - observe: { $0 }, - removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } - ) { viewStore in - ForEach(viewStore.state, id: viewStore.state.id) { element in - let id = element[keyPath: viewStore.state.id] - var element = element - content( - store.scope( - id: store.id(state: \.[id:id]!, action: \.[id:id]), - state: ToState { - element = $0[id: id] ?? element - return element - }, - action: { .element(id: id, action: $0) }, - isInvalid: { !$0.ids.contains(id) } - ) - ) - } - } - } - - @available( - iOS, - deprecated: 9999, - message: - "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" - ) - @available( - macOS, - deprecated: 9999, - message: - "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" - ) - @available( - tvOS, - deprecated: 9999, - message: - "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" - ) - @available( - watchOS, - deprecated: 9999, - message: - "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, (id: ID, action: EachAction)>, - @ViewBuilder content: @escaping (_ store: Store) -> EachContent - ) - where - Data == IdentifiedArray, - Content == WithViewStore< - IdentifiedArray, (id: ID, action: EachAction), - ForEach, ID, EachContent> - > - { - self.data = store.withState { $0 } - self.content = WithViewStore( - store, - observe: { $0 }, - removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } - ) { viewStore in - ForEach(viewStore.state, id: viewStore.state.id) { element in - var element = element - let id = element[keyPath: viewStore.state.id] - content( - store.scope( - id: store.id(state: \.[id:id]!, action: \.[id:id]), - state: ToState { - element = $0[id: id] ?? element - return element - }, - action: { (id, $0) }, - isInvalid: { !$0.ids.contains(id) } - ) - ) - } - } - } - - public var body: some View { - self.content - } -} - -#if compiler(>=6) - extension ForEachStore: @preconcurrency DynamicViewContent {} -#else - extension ForEachStore: DynamicViewContent {} -#endif - -extension Case { - fileprivate subscript(id id: ID) -> Case - where Value == (id: ID, action: Action) { - Case( - embed: { (id: id, action: $0) }, - extract: { $0.id == id ? $0.action : nil } - ) - } -} +//import OrderedCollections +//import SwiftUI +// +///// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with +///// collections of state. +///// +///// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each +///// element. This allows you to extract and modularize an element's view and avoid concerns around +///// collection index math and parent-child store communication. +///// +///// For example, a todos app may define the domain and logic associated with an individual todo: +///// +///// ```swift +///// @Reducer +///// struct Todo { +///// struct State: Equatable, Identifiable { +///// let id: UUID +///// var description = "" +///// var isComplete = false +///// } +///// +///// enum Action { +///// case isCompleteToggled(Bool) +///// case descriptionChanged(String) +///// } +///// +///// var body: some Reducer { +///// // ... +///// } +///// } +///// ``` +///// +///// As well as a view with a domain-specific store: +///// +///// ```swift +///// struct TodoView: View { +///// let store: StoreOf +///// var body: some View { /* ... */ } +///// } +///// ``` +///// +///// For a parent domain to work with a collection of todos, it can hold onto this collection in +///// state: +///// +///// ```swift +///// @Reducer +///// struct Todos { +///// struct State: Equatable { +///// var todos: IdentifiedArrayOf = [] +///// } +///// // ... +///// } +///// ``` +///// +///// Define a case to handle actions sent to the child domain: +///// +///// ```swift +///// enum Action { +///// case todos(IdentifiedActionOf) +///// } +///// ``` +///// +///// Enhance its core reducer using +///// ``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-3dw7i``: +///// +///// ```swift +///// var body: some Reducer { +///// Reduce { state, action in +///// // ... +///// } +///// .forEach(\.todos, action: \.todos) { +///// Todo() +///// } +///// } +///// ``` +///// +///// And finally render a list of `TodoView`s using ``ForEachStore``: +///// +///// ```swift +///// ForEachStore( +///// self.store.scope(state: \.todos, action: \.todos) +///// ) { todoStore in +///// TodoView(store: todoStore) +///// } +///// ``` +///// +//@available( +// iOS, deprecated: 9999, +// message: +// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +//) +//@available( +// macOS, deprecated: 9999, +// message: +// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +//) +//@available( +// tvOS, deprecated: 9999, +// message: +// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +//) +//@available( +// watchOS, deprecated: 9999, +// message: +// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +//) +//public struct ForEachStore< +// EachState, EachAction, Data: Collection, ID: Hashable & Sendable, Content: View +//>: View { +// public let data: Data +// let content: Content +// +// /// Initializes a structure that computes views on demand from a store on a collection of data and +// /// an identified action. +// /// +// /// - Parameters: +// /// - store: A store on an identified array of data and an identified action. +// /// - content: A function that can generate content given a store of an element. +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, IdentifiedAction>, +// @ViewBuilder content: @escaping (_ store: Store) -> EachContent +// ) +// where +// Data == IdentifiedArray, +// Content == WithViewStore< +// IdentifiedArray, IdentifiedAction, +// ForEach, ID, EachContent> +// > +// { +// self.data = store.withState { $0 } +// self.content = WithViewStore( +// store, +// observe: { $0 }, +// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } +// ) { viewStore in +// ForEach(viewStore.state, id: viewStore.state.id) { element in +// let id = element[keyPath: viewStore.state.id] +// var element = element +// content( +// store.scope( +// id: store.id(state: \.[id:id]!, action: \.[id:id]), +// state: ToState { +// element = $0[id: id] ?? element +// return element +// }, +// action: { .element(id: id, action: $0) }, +// isInvalid: { !$0.ids.contains(id) } +// ) +// ) +// } +// } +// } +// +// @available( +// iOS, +// deprecated: 9999, +// message: +// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" +// ) +// @available( +// macOS, +// deprecated: 9999, +// message: +// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" +// ) +// @available( +// tvOS, +// deprecated: 9999, +// message: +// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" +// ) +// @available( +// watchOS, +// deprecated: 9999, +// message: +// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, (id: ID, action: EachAction)>, +// @ViewBuilder content: @escaping (_ store: Store) -> EachContent +// ) +// where +// Data == IdentifiedArray, +// Content == WithViewStore< +// IdentifiedArray, (id: ID, action: EachAction), +// ForEach, ID, EachContent> +// > +// { +// self.data = store.withState { $0 } +// self.content = WithViewStore( +// store, +// observe: { $0 }, +// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } +// ) { viewStore in +// ForEach(viewStore.state, id: viewStore.state.id) { element in +// var element = element +// let id = element[keyPath: viewStore.state.id] +// content( +// store.scope( +// id: store.id(state: \.[id:id]!, action: \.[id:id]), +// state: ToState { +// element = $0[id: id] ?? element +// return element +// }, +// action: { (id, $0) }, +// isInvalid: { !$0.ids.contains(id) } +// ) +// ) +// } +// } +// } +// +// public var body: some View { +// self.content +// } +//} +// +//#if compiler(>=6) +// extension ForEachStore: @preconcurrency DynamicViewContent {} +//#else +// extension ForEachStore: DynamicViewContent {} +//#endif +// +//extension Case { +// fileprivate subscript(id id: ID) -> Case +// where Value == (id: ID, action: Action) { +// Case( +// embed: { (id: id, action: $0) }, +// extract: { $0.id == id ? $0.action : nil } +// ) +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift b/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift index b4f5acb11c96..19f4d876e11d 100644 --- a/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift +++ b/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift @@ -1,107 +1,107 @@ -import SwiftUI - -#if !os(macOS) - @available(iOS 14, tvOS 14, watchOS 7, *) - @available(macOS, unavailable) - extension View { - /// Presents a modal view that covers as much of the screen as possible using the store you - /// provide as a data source for the sheet's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view - /// > modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a sheet - /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the - /// system dismisses the currently displayed sheet. - /// - onDismiss: The closure to execute when dismissing the modal view. - /// - content: A closure returning the content of the modal view. - @available( - iOS, deprecated: 9999, - message: - "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - macOS, deprecated: 9999, - message: - "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - tvOS, deprecated: 9999, - message: - "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - watchOS, deprecated: 9999, - message: - "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - public func fullScreenCover( - store: Store, PresentationAction>, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (_ store: Store) -> Content - ) -> some View { - self.presentation(store: store) { `self`, $item, destination in - self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in - destination(content) - } - } - } - - /// Presents a modal view that covers as much of the screen as possible using the store you - /// provide as a data source for the sheet's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view - /// > modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a sheet - /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the - /// system dismisses the currently displayed sheet. - /// - toDestinationState: A transformation to extract modal state from the presentation state. - /// - fromDestinationAction: A transformation to embed modal actions into the presentation - /// action. - /// - onDismiss: The closure to execute when dismissing the modal view. - /// - content: A closure returning the content of the modal view. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - public func fullScreenCover( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (_ store: Store) -> - Content - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, destination in - self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in - destination(content) - } - } - } - } -#endif +//import SwiftUI +// +//#if !os(macOS) +// @available(iOS 14, tvOS 14, watchOS 7, *) +// @available(macOS, unavailable) +// extension View { +// /// Presents a modal view that covers as much of the screen as possible using the store you +// /// provide as a data source for the sheet's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view +// /// > modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet +// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the +// /// system dismisses the currently displayed sheet. +// /// - onDismiss: The closure to execute when dismissing the modal view. +// /// - content: A closure returning the content of the modal view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// public func fullScreenCover( +// store: Store, PresentationAction>, +// onDismiss: (() -> Void)? = nil, +// @ViewBuilder content: @escaping (_ store: Store) -> Content +// ) -> some View { +// self.presentation(store: store) { `self`, $item, destination in +// self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in +// destination(content) +// } +// } +// } +// +// /// Presents a modal view that covers as much of the screen as possible using the store you +// /// provide as a data source for the sheet's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view +// /// > modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet +// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the +// /// system dismisses the currently displayed sheet. +// /// - toDestinationState: A transformation to extract modal state from the presentation state. +// /// - fromDestinationAction: A transformation to embed modal actions into the presentation +// /// action. +// /// - onDismiss: The closure to execute when dismissing the modal view. +// /// - content: A closure returning the content of the modal view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// public func fullScreenCover( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// onDismiss: (() -> Void)? = nil, +// @ViewBuilder content: @escaping (_ store: Store) -> +// Content +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, destination in +// self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in +// destination(content) +// } +// } +// } +// } +//#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift index 5cb33a2a7341..35f1dadf1b73 100644 --- a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift @@ -1,306 +1,306 @@ -import SwiftUI - -/// A view that safely unwraps a store of optional state in order to show one of two views. -/// -/// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` -/// that holds onto non-optional state, and otherwise the `else` closure will be performed. -/// -/// This is useful for deciding between two views to show depending on an optional piece of state: -/// -/// ```swift -/// IfLetStore( -/// store.scope(state: \.results, action: { .results($0) }) -/// ) { -/// SearchResultsView(store: $0) -/// } else: { -/// Text("Loading search results...") -/// } -/// ``` -/// -@available( - iOS, deprecated: 9999, - message: - "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -) -@available( - macOS, deprecated: 9999, - message: - "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -) -@available( - tvOS, deprecated: 9999, - message: - "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -) -@available( - watchOS, deprecated: 9999, - message: - "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -) -public struct IfLetStore: View { - private let content: (ViewStore) -> Content - private let store: Store - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional - /// state is `nil` or non-`nil`. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil`. - /// - elseContent: A view that is only visible when the optional state is `nil`. - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, - @ViewBuilder else elseContent: () -> ElseContent - ) where Content == _ConditionalContent { - let store = store.scope( - id: store.id(state: \.self, action: \.self), - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0 == nil } - ) - self.store = store - let elseContent = elseContent() - self.content = { viewStore in - if var state = viewStore.state { - return ViewBuilder.buildEither( - first: ifContent( - store.scope( - id: store.id(state: \.!, action: \.self), - state: ToState { - state = $0 ?? state - return state - }, - action: { $0 }, - isInvalid: { $0 == nil } - ) - ) - ) - } else { - return ViewBuilder.buildEither(second: elseContent) - } - } - } - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional - /// state is `nil` or non-`nil`. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil`. - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent - ) where Content == IfContent? { - let store = store.scope( - id: store.id(state: \.self, action: \.self), - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0 == nil } - ) - self.store = store - self.content = { viewStore in - if var state = viewStore.state { - return ifContent( - store.scope( - id: store.id(state: \.!, action: \.self), - state: ToState { - state = $0 ?? state - return state - }, - action: { $0 }, - isInvalid: { $0 == nil } - ) - ) - } else { - return nil - } - } - } - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of - /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil`. - /// - elseContent: A view that is only visible when the optional state is `nil`. - @available( - iOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, PresentationAction>, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, - @ViewBuilder else elseContent: @escaping () -> ElseContent - ) where Content == _ConditionalContent { - self.init( - store.scope(state: \.wrappedValue, action: \.presented), - then: ifContent, - else: elseContent - ) - } - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of - /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil`. - @available( - iOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, PresentationAction>, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent - ) where Content == IfContent? { - self.init( - store.scope(state: \.wrappedValue, action: \.presented), - then: ifContent - ) - } - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of - /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further - /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - toState: A closure that attempts to extract state for the "if" branch from the destination - /// state. - /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil` and state can be extracted from the - /// destination state. - /// - elseContent: A view that is only visible when state cannot be extracted from the - /// destination. - @available( - *, deprecated, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, PresentationAction>, - state toState: @escaping (_ destinationState: DestinationState) -> State?, - action fromAction: @escaping (_ action: Action) -> DestinationAction, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, - @ViewBuilder else elseContent: @escaping () -> ElseContent - ) where Content == _ConditionalContent { - self.init( - store.scope( - state: { $0.wrappedValue.flatMap(toState) }, - action: { .presented(fromAction($0)) } - ), - then: ifContent, - else: elseContent - ) - } - - /// Initializes an ``IfLetStore`` view that computes content depending on if a store of - /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further - /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. - /// - /// - Parameters: - /// - store: A store of optional state. - /// - toState: A closure that attempts to extract state for the "if" branch from the destination - /// state. - /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. - /// - ifContent: A function that is given a store of non-optional state and returns a view that - /// is visible only when the optional state is non-`nil` and state can be extracted from the - /// destination state. - @available( - *, deprecated, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public init( - _ store: Store, PresentationAction>, - state toState: @escaping (_ destinationState: DestinationState) -> State?, - action fromAction: @escaping (_ action: Action) -> DestinationAction, - @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent - ) where Content == IfContent? { - self.init( - store.scope( - state: { $0.wrappedValue.flatMap(toState) }, - action: { .presented(fromAction($0)) } - ), - then: ifContent - ) - } - - public var body: some View { - WithViewStore( - self.store, - observe: { $0 }, - removeDuplicates: { ($0 != nil) == ($1 != nil) }, - content: self.content - ) - } -} +//import SwiftUI +// +///// A view that safely unwraps a store of optional state in order to show one of two views. +///// +///// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` +///// that holds onto non-optional state, and otherwise the `else` closure will be performed. +///// +///// This is useful for deciding between two views to show depending on an optional piece of state: +///// +///// ```swift +///// IfLetStore( +///// store.scope(state: \.results, action: { .results($0) }) +///// ) { +///// SearchResultsView(store: $0) +///// } else: { +///// Text("Loading search results...") +///// } +///// ``` +///// +//@available( +// iOS, deprecated: 9999, +// message: +// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +//) +//@available( +// macOS, deprecated: 9999, +// message: +// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +//) +//@available( +// tvOS, deprecated: 9999, +// message: +// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +//) +//@available( +// watchOS, deprecated: 9999, +// message: +// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +//) +//public struct IfLetStore: View { +// private let content: (ViewStore) -> Content +// private let store: Store +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional +// /// state is `nil` or non-`nil`. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil`. +// /// - elseContent: A view that is only visible when the optional state is `nil`. +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, +// @ViewBuilder else elseContent: () -> ElseContent +// ) where Content == _ConditionalContent { +// let store = store.scope( +// id: store.id(state: \.self, action: \.self), +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0 == nil } +// ) +// self.store = store +// let elseContent = elseContent() +// self.content = { viewStore in +// if var state = viewStore.state { +// return ViewBuilder.buildEither( +// first: ifContent( +// store.scope( +// id: store.id(state: \.!, action: \.self), +// state: ToState { +// state = $0 ?? state +// return state +// }, +// action: { $0 }, +// isInvalid: { $0 == nil } +// ) +// ) +// ) +// } else { +// return ViewBuilder.buildEither(second: elseContent) +// } +// } +// } +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional +// /// state is `nil` or non-`nil`. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil`. +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent +// ) where Content == IfContent? { +// let store = store.scope( +// id: store.id(state: \.self, action: \.self), +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0 == nil } +// ) +// self.store = store +// self.content = { viewStore in +// if var state = viewStore.state { +// return ifContent( +// store.scope( +// id: store.id(state: \.!, action: \.self), +// state: ToState { +// state = $0 ?? state +// return state +// }, +// action: { $0 }, +// isInvalid: { $0 == nil } +// ) +// ) +// } else { +// return nil +// } +// } +// } +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of +// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil`. +// /// - elseContent: A view that is only visible when the optional state is `nil`. +// @available( +// iOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, PresentationAction>, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, +// @ViewBuilder else elseContent: @escaping () -> ElseContent +// ) where Content == _ConditionalContent { +// self.init( +// store.scope(state: \.wrappedValue, action: \.presented), +// then: ifContent, +// else: elseContent +// ) +// } +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of +// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil`. +// @available( +// iOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, PresentationAction>, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent +// ) where Content == IfContent? { +// self.init( +// store.scope(state: \.wrappedValue, action: \.presented), +// then: ifContent +// ) +// } +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of +// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further +// /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - toState: A closure that attempts to extract state for the "if" branch from the destination +// /// state. +// /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil` and state can be extracted from the +// /// destination state. +// /// - elseContent: A view that is only visible when state cannot be extracted from the +// /// destination. +// @available( +// *, deprecated, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, PresentationAction>, +// state toState: @escaping (_ destinationState: DestinationState) -> State?, +// action fromAction: @escaping (_ action: Action) -> DestinationAction, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, +// @ViewBuilder else elseContent: @escaping () -> ElseContent +// ) where Content == _ConditionalContent { +// self.init( +// store.scope( +// state: { $0.wrappedValue.flatMap(toState) }, +// action: { .presented(fromAction($0)) } +// ), +// then: ifContent, +// else: elseContent +// ) +// } +// +// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of +// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further +// /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. +// /// +// /// - Parameters: +// /// - store: A store of optional state. +// /// - toState: A closure that attempts to extract state for the "if" branch from the destination +// /// state. +// /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. +// /// - ifContent: A function that is given a store of non-optional state and returns a view that +// /// is visible only when the optional state is non-`nil` and state can be extracted from the +// /// destination state. +// @available( +// *, deprecated, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public init( +// _ store: Store, PresentationAction>, +// state toState: @escaping (_ destinationState: DestinationState) -> State?, +// action fromAction: @escaping (_ action: Action) -> DestinationAction, +// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent +// ) where Content == IfContent? { +// self.init( +// store.scope( +// state: { $0.wrappedValue.flatMap(toState) }, +// action: { .presented(fromAction($0)) } +// ), +// then: ifContent +// ) +// } +// +// public var body: some View { +// WithViewStore( +// self.store, +// observe: { $0 }, +// removeDuplicates: { ($0 != nil) == ($1 != nil) }, +// content: self.content +// ) +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift b/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift index c819d0aab380..4bce897865c9 100644 --- a/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift +++ b/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift @@ -1,130 +1,130 @@ -@_spi(Reflection) import CasePaths -import SwiftUI - -@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -extension View { - /// Associates a destination view with a store that can be used to push the view onto a - /// `NavigationStack`. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's - /// > `navigationDestination(isPresented:)` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped - /// `State` and `Action` to the modifier's closure. You use this store to power the content - /// in a view that the system pushes onto the navigation stack. If `store`'s state is - /// `nil`-ed out, the system pops the view from the stack. - /// - destination: A closure returning the content of the destination view. - @available( - iOS, deprecated: 9999, - message: - "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - macOS, deprecated: 9999, - message: - "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - tvOS, deprecated: 9999, - message: - "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - watchOS, deprecated: 9999, - message: - "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func navigationDestination( - store: Store, PresentationAction>, - @ViewBuilder destination: @escaping (_ store: Store) -> Destination - ) -> some View { - self.presentation( - store: store, - id: { $0.wrappedValue.map(NavigationDestinationID.init) } - ) { `self`, $item, destinationContent in - self.navigationDestination(isPresented: Binding($item)) { - destinationContent(destination) - } - } - } - - /// Associates a destination view with a store that can be used to push the view onto a - /// `NavigationStack`. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's - /// > `navigationDestination(isPresented:)` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped - /// `State` and `Action` to the modifier's closure. You use this store to power the content - /// in a view that the system pushes onto the navigation stack. If `store`'s state is - /// `nil`-ed out, the system pops the view from the stack. - /// - toDestinationState: A transformation to extract screen state from the presentation - /// state. - /// - fromDestinationAction: A transformation to embed screen actions into the presentation - /// action. - /// - destination: A closure returning the content of the destination view. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func navigationDestination< - State, Action, DestinationState, DestinationAction, Destination: View - >( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - @ViewBuilder destination: @escaping (_ store: Store) -> - Destination - ) -> some View { - self.presentation( - store: store, - state: toDestinationState, - id: { $0.wrappedValue.map(NavigationDestinationID.init) }, - action: fromDestinationAction - ) { `self`, $item, destinationContent in - self.navigationDestination(isPresented: Binding($item)) { - destinationContent(destination) - } - } - } -} - -private struct NavigationDestinationID: Hashable { - let objectIdentifier: ObjectIdentifier - let enumTag: UInt32? - - init(_ value: Value) { - self.objectIdentifier = ObjectIdentifier(Value.self) - self.enumTag = EnumMetadata(Value.self)?.tag(of: value) - } -} +//@_spi(Reflection) import CasePaths +//import SwiftUI +// +//@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +//extension View { +// /// Associates a destination view with a store that can be used to push the view onto a +// /// `NavigationStack`. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's +// /// > `navigationDestination(isPresented:)` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped +// /// `State` and `Action` to the modifier's closure. You use this store to power the content +// /// in a view that the system pushes onto the navigation stack. If `store`'s state is +// /// `nil`-ed out, the system pops the view from the stack. +// /// - destination: A closure returning the content of the destination view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func navigationDestination( +// store: Store, PresentationAction>, +// @ViewBuilder destination: @escaping (_ store: Store) -> Destination +// ) -> some View { +// self.presentation( +// store: store, +// id: { $0.wrappedValue.map(NavigationDestinationID.init) } +// ) { `self`, $item, destinationContent in +// self.navigationDestination(isPresented: Binding($item)) { +// destinationContent(destination) +// } +// } +// } +// +// /// Associates a destination view with a store that can be used to push the view onto a +// /// `NavigationStack`. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's +// /// > `navigationDestination(isPresented:)` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped +// /// `State` and `Action` to the modifier's closure. You use this store to power the content +// /// in a view that the system pushes onto the navigation stack. If `store`'s state is +// /// `nil`-ed out, the system pops the view from the stack. +// /// - toDestinationState: A transformation to extract screen state from the presentation +// /// state. +// /// - fromDestinationAction: A transformation to embed screen actions into the presentation +// /// action. +// /// - destination: A closure returning the content of the destination view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func navigationDestination< +// State, Action, DestinationState, DestinationAction, Destination: View +// >( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// @ViewBuilder destination: @escaping (_ store: Store) -> +// Destination +// ) -> some View { +// self.presentation( +// store: store, +// state: toDestinationState, +// id: { $0.wrappedValue.map(NavigationDestinationID.init) }, +// action: fromDestinationAction +// ) { `self`, $item, destinationContent in +// self.navigationDestination(isPresented: Binding($item)) { +// destinationContent(destination) +// } +// } +// } +//} +// +//private struct NavigationDestinationID: Hashable { +// let objectIdentifier: ObjectIdentifier +// let enumTag: UInt32? +// +// init(_ value: Value) { +// self.objectIdentifier = ObjectIdentifier(Value.self) +// self.enumTag = EnumMetadata(Value.self)?.tag(of: value) +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift b/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift index 6ab479e9b1f8..20b1f2e7241b 100644 --- a/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift @@ -1,172 +1,209 @@ -import OrderedCollections -import SwiftUI - -/// A navigation stack that is driven by a store. -/// -/// This view can be used to drive stack-based navigation in the Composable Architecture when passed -/// a store that is focused on ``StackState`` and ``StackAction``. -/// -/// See the dedicated article on for more information on the library's navigation -/// tools, and in particular see for information on using this view. -@available( - iOS, deprecated: 9999, - message: - "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -) -@available( - macOS, deprecated: 9999, - message: - "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -) -@available( - tvOS, deprecated: 9999, - message: - "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -) -@available( - watchOS, deprecated: 9999, - message: - "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -) -@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -public struct NavigationStackStore: View { - private let root: Root - private let destination: (StackState.Component) -> Destination - @ObservedObject private var viewStore: ViewStore, StackAction> - - /// Creates a navigation stack with a store of stack state and actions. - /// - /// - Parameters: - /// - path: A store of stack state and actions to power this stack. - /// - root: The view to display when the stack is empty. - /// - destination: A view builder that defines a view to display when an element is appended to - /// the stack's state. The closure takes one argument, which is a store of the value to - /// present. - public init( - _ store: Store, StackAction>, - @ViewBuilder root: () -> Root, - @ViewBuilder destination: @escaping (_ store: Store) -> Destination, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) { - self.root = root() - self.destination = { component in - var element = component.element - return destination( - store - .scope( - id: store.id( - state: - \.[ - id:component.id,fileID:_HashableStaticString( - rawValue: fileID),filePath:_HashableStaticString( - rawValue: filePath),line:line,column:column - ]!, - action: \.[id:component.id] - ), - state: ToState { - element = $0[id: component.id] ?? element - return element - }, - action: { .element(id: component.id, action: $0) }, - isInvalid: { !$0.ids.contains(component.id) } - ) - ) - } - self._viewStore = ObservedObject( - wrappedValue: ViewStore( - store, - observe: { $0 }, - removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } - ) - ) - } - - /// Creates a navigation stack with a store of stack state and actions. - /// - /// - Parameters: - /// - path: A store of stack state and actions to power this stack. - /// - root: The view to display when the stack is empty. - /// - destination: A view builder that defines a view to display when an element is appended to - /// the stack's state. The closure takes one argument, which is the initial enum state to - /// present. You can switch over this value and use ``CaseLet`` views to handle each case. - @_disfavoredOverload - public init( - _ store: Store, StackAction>, - @ViewBuilder root: () -> Root, - @ViewBuilder destination: @escaping (_ initialState: State) -> D, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) where Destination == SwitchStore { - self.root = root() - self.destination = { component in - var element = component.element - return SwitchStore( - store - .scope( - id: store.id( - state: - \.[ - id:component.id,fileID:_HashableStaticString( - rawValue: fileID),filePath:_HashableStaticString( - rawValue: filePath),line:line,column:column - ]!, - action: \.[id:component.id] - ), - state: ToState { - element = $0[id: component.id] ?? element - return element - }, - action: { .element(id: component.id, action: $0) }, - isInvalid: { !$0.ids.contains(component.id) } - ) - ) { _ in - destination(component.element) - } - } - self._viewStore = ObservedObject( - wrappedValue: ViewStore( - store, - observe: { $0 }, - removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } - ) - ) - } - - public var body: some View { - NavigationStack( - path: self.viewStore.binding( - get: { $0.path }, - compactSend: { newPath in - if newPath.count > self.viewStore.path.count, let component = newPath.last { - return .push(id: component.id, state: component.element) - } else if newPath.count < self.viewStore.path.count { - return .popFrom(id: self.viewStore.path[newPath.count].id) - } else { - return nil - } - } - ) - ) { - self.root - .environment(\.navigationDestinationType, State.self) - .navigationDestination(for: StackState.Component.self) { component in - NavigationDestinationView(component: component, destination: self.destination) - } - } - } -} - -private struct NavigationDestinationView: View { - let component: StackState.Component - let destination: (StackState.Component) -> Destination - var body: some View { - self.destination(self.component) - .environment(\.navigationDestinationType, State.self) - .id(self.component.id) - } -} +//import OrderedCollections +//import SwiftUI +// +///// A navigation stack that is driven by a store. +///// +///// This view can be used to drive stack-based navigation in the Composable Architecture when passed +///// a store that is focused on ``StackState`` and ``StackAction``. +///// +///// See the dedicated article on for more information on the library's navigation +///// tools, and in particular see for information on using this view. +//@available( +// iOS, deprecated: 9999, +// message: +// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +//) +//@available( +// macOS, deprecated: 9999, +// message: +// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +//) +//@available( +// tvOS, deprecated: 9999, +// message: +// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +//) +//@available( +// watchOS, deprecated: 9999, +// message: +// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +//) +//@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +//public struct NavigationStackStore: View { +// private let root: Root +// private let destination: (StackState.Component) -> Destination +// @ObservedObject private var viewStore: ViewStore, StackAction> +// +// /// Creates a navigation stack with a store of stack state and actions. +// /// +// /// - Parameters: +// /// - path: A store of stack state and actions to power this stack. +// /// - root: The view to display when the stack is empty. +// /// - destination: A view builder that defines a view to display when an element is appended to +// /// the stack's state. The closure takes one argument, which is a store of the value to +// /// present. +// public init( +// _ store: Store, StackAction>, +// @ViewBuilder root: () -> Root, +// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, +// fileID: StaticString = #fileID, +// filePath: StaticString = #filePath, +// line: UInt = #line, +// column: UInt = #column +// ) { +// func navigationDestination( +// component: StackState.Component +// ) -> Destination { +// let id = store.id( +// state: +// \.[ +// id:component.id, +// fileID:_HashableStaticString(rawValue: fileID), +// filePath:_HashableStaticString(rawValue: filePath), +// line:line, +// column:column +// ], +// action: \.[id:component.id] +// ) +// if let child = store.children[id] as? Store { +// return destination(child) +// } else { +// @MainActor +// func open( +// _ core: some Core, StackAction> +// ) -> Destination { +// let child = Store( +// core: IfLetCore( +// base: core, +// cachedState: component.element, +// stateKeyPath: \.[ +// id:component.id, +// fileID:_HashableStaticString(rawValue: fileID), +// filePath:_HashableStaticString(rawValue: filePath), +// line:line, +// column:column +// ], +// actionKeyPath: \.[id:component.id] +// ) +// ) +// store.children[id] = child +// return destination(child) +// } +// return open(store.core) +// } +// } +// self.root = root() +// self.destination = navigationDestination(component:) +// self._viewStore = ObservedObject( +// wrappedValue: ViewStore( +// store, +// observe: { $0 }, +// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } +// ) +// ) +// } +// +// /// Creates a navigation stack with a store of stack state and actions. +// /// +// /// - Parameters: +// /// - path: A store of stack state and actions to power this stack. +// /// - root: The view to display when the stack is empty. +// /// - destination: A view builder that defines a view to display when an element is appended to +// /// the stack's state. The closure takes one argument, which is the initial enum state to +// /// present. You can switch over this value and use ``CaseLet`` views to handle each case. +// @_disfavoredOverload +// public init( +// _ store: Store, StackAction>, +// @ViewBuilder root: () -> Root, +// @ViewBuilder destination: @escaping (_ initialState: State) -> D, +// fileID: StaticString = #fileID, +// filePath: StaticString = #filePath, +// line: UInt = #line, +// column: UInt = #column +// ) where Destination == SwitchStore { +// func navigationDestination( +// component: StackState.Component +// ) -> Destination { +// let id = store.id( +// state: +// \.[ +// id:component.id, +// fileID:_HashableStaticString(rawValue: fileID), +// filePath:_HashableStaticString(rawValue: filePath), +// line:line, +// column:column +// ], +// action: \.[id:component.id] +// ) +// if let child = store.children[id] as? Store { +// return SwitchStore(child, content: destination) +// } else { +// @MainActor +// func open( +// _ core: some Core, StackAction> +// ) -> Destination { +// let child = Store( +// core: IfLetCore( +// base: core, +// cachedState: component.element, +// stateKeyPath: \.[ +// id:component.id, +// fileID:_HashableStaticString(rawValue: fileID), +// filePath:_HashableStaticString(rawValue: filePath), +// line:line, +// column:column +// ], +// actionKeyPath: \.[id:component.id] +// ) +// ) +// store.children[id] = child +// return SwitchStore(child, content: destination) +// } +// return open(store.core) +// } +// } +// +// self.root = root() +// self.destination = navigationDestination(component:) +// self._viewStore = ObservedObject( +// wrappedValue: ViewStore( +// store, +// observe: { $0 }, +// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } +// ) +// ) +// } +// +// public var body: some View { +// NavigationStack( +// path: self.viewStore.binding( +// get: { $0.path }, +// compactSend: { newPath in +// if newPath.count > self.viewStore.path.count, let component = newPath.last { +// return .push(id: component.id, state: component.element) +// } else if newPath.count < self.viewStore.path.count { +// return .popFrom(id: self.viewStore.path[newPath.count].id) +// } else { +// return nil +// } +// } +// ) +// ) { +// self.root +// .environment(\.navigationDestinationType, State.self) +// .navigationDestination(for: StackState.Component.self) { component in +// NavigationDestinationView(component: component, destination: self.destination) +// } +// } +// } +//} +// +//private struct NavigationDestinationView: View { +// let component: StackState.Component +// let destination: (StackState.Component) -> Destination +// var body: some View { +// self.destination(self.component) +// .environment(\.navigationDestinationType, State.self) +// .id(self.component.id) +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Popover.swift b/Sources/ComposableArchitecture/SwiftUI/Popover.swift index b4caf45bd242..e42640d770c7 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Popover.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Popover.swift @@ -1,116 +1,116 @@ -import SwiftUI - -@available(tvOS, unavailable) -@available(watchOS, unavailable) -extension View { - /// Presents a popover using the given store as a data source for the popover's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a - /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed - /// out, the system dismisses the currently displayed popover. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow in macOS. iOS ignores this parameter. - /// - content: A closure returning the content of the popover. - @available( - iOS, deprecated: 9999, - message: - "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - macOS, deprecated: 9999, - message: - "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - tvOS, deprecated: 9999, - message: - "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - watchOS, deprecated: 9999, - message: - "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func popover( - store: Store, PresentationAction>, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (_ store: Store) -> Content - ) -> some View { - self.presentation(store: store) { `self`, $item, destination in - self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in - destination(content) - } - } - } - - /// Presents a popover using the given store as a data source for the popover's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a - /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed - /// out, the system dismisses the currently displayed popover. - /// - toDestinationState: A transformation to extract popover state from the presentation state. - /// - fromDestinationAction: A transformation to embed popover actions into the presentation - /// action. - /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. - /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's - /// arrow in macOS. iOS ignores this parameter. - /// - content: A closure returning the content of the popover. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func popover( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), - arrowEdge: Edge = .top, - @ViewBuilder content: @escaping (_ store: Store) -> Content - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, destination in - self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in - destination(content) - } - } - } -} +//import SwiftUI +// +//@available(tvOS, unavailable) +//@available(watchOS, unavailable) +//extension View { +// /// Presents a popover using the given store as a data source for the popover's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a +// /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed +// /// out, the system dismisses the currently displayed popover. +// /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. +// /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's +// /// arrow in macOS. iOS ignores this parameter. +// /// - content: A closure returning the content of the popover. +// @available( +// iOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func popover( +// store: Store, PresentationAction>, +// attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), +// arrowEdge: Edge = .top, +// @ViewBuilder content: @escaping (_ store: Store) -> Content +// ) -> some View { +// self.presentation(store: store) { `self`, $item, destination in +// self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in +// destination(content) +// } +// } +// } +// +// /// Presents a popover using the given store as a data source for the popover's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a +// /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed +// /// out, the system dismisses the currently displayed popover. +// /// - toDestinationState: A transformation to extract popover state from the presentation state. +// /// - fromDestinationAction: A transformation to embed popover actions into the presentation +// /// action. +// /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. +// /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's +// /// arrow in macOS. iOS ignores this parameter. +// /// - content: A closure returning the content of the popover. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func popover( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), +// arrowEdge: Edge = .top, +// @ViewBuilder content: @escaping (_ store: Store) -> Content +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, destination in +// self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in +// destination(content) +// } +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift index df8ed127de5f..1ba4b920bb61 100644 --- a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift +++ b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift @@ -1,360 +1,360 @@ -import SwiftUI - -extension View { - @_spi(Presentation) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation( - store: Store, PresentationAction>, - @ViewBuilder body: @escaping ( - _ content: Self, - _ isPresented: Binding, - _ destination: DestinationContent - ) -> Content - ) -> some View { - self.presentation(store: store) { `self`, $item, destination in - body(self, Binding($item), destination) - } - } - - @_disfavoredOverload - @_spi(Presentation) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation( - store: Store, PresentationAction>, - @ViewBuilder body: @escaping ( - _ content: Self, - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) -> some View { - self.presentation( - store: store, - id: { $0.wrappedValue.map { _ in ObjectIdentifier(State.self) } } - ) { `self`, $item, destination in - body(self, $item, destination) - } - } - - @_disfavoredOverload - @_spi(Presentation) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation( - store: Store, PresentationAction>, - id toID: @escaping (PresentationState) -> AnyHashable?, - @ViewBuilder body: @escaping ( - _ content: Self, - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) -> some View { - PresentationStore(store, id: toID) { $item, destination in - body(self, $item, destination) - } - } - - @_spi(Presentation) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation< - State, - Action, - DestinationState, - DestinationAction, - Content: View - >( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - @ViewBuilder body: @escaping ( - _ content: Self, - _ isPresented: Binding, - _ destination: DestinationContent - ) -> Content - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, destination in - body(self, Binding($item), destination) - } - } - - @_disfavoredOverload - @_spi(Presentation) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation< - State, - Action, - DestinationState, - DestinationAction, - Content: View - >( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - @ViewBuilder body: @escaping ( - _ content: Self, - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) -> some View { - self.presentation( - store: store, - state: toDestinationState, - id: { $0.id }, - action: fromDestinationAction, - body: body - ) - } - - @_spi(Presentation) - @ViewBuilder - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func presentation< - State, - Action, - DestinationState, - DestinationAction, - Content: View - >( - store: Store, PresentationAction>, - state toDestinationState: @escaping (State) -> DestinationState?, - id toID: @escaping (PresentationState) -> AnyHashable?, - action fromDestinationAction: @escaping (DestinationAction) -> Action, - @ViewBuilder body: @escaping ( - Self, - Binding, - DestinationContent - ) -> Content - ) -> some View { - PresentationStore( - store, state: toDestinationState, id: toID, action: fromDestinationAction - ) { $item, destination in - body(self, $item, destination) - } - } -} - -@_spi(Presentation) -public struct PresentationStore< - State, Action, DestinationState, DestinationAction, Content: View ->: View { - let store: Store, PresentationAction> - let toDestinationState: (State) -> DestinationState? - let toID: (PresentationState) -> AnyHashable? - let fromDestinationAction: (DestinationAction) -> Action - let destinationStore: Store - let content: - ( - Binding, - DestinationContent - ) -> Content - - @ObservedObject var viewStore: ViewStore, PresentationAction> - - public init( - _ store: Store, PresentationAction>, - @ViewBuilder content: @escaping ( - _ isPresented: Binding, - _ destination: DestinationContent - ) -> Content - ) where State == DestinationState, Action == DestinationAction { - self.init(store) { $item, destination in - content(Binding($item), destination) - } - } - - @_disfavoredOverload - public init( - _ store: Store, PresentationAction>, - @ViewBuilder content: @escaping ( - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) where State == DestinationState, Action == DestinationAction { - self.init( - store, - id: { $0.id }, - content: content - ) - } - - public init( - _ store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - @ViewBuilder content: @escaping ( - _ isPresented: Binding, - _ destination: DestinationContent - ) -> Content - ) { - self.init( - store, state: toDestinationState, action: fromDestinationAction - ) { $item, destination in - content(Binding($item), destination) - } - } - - @_disfavoredOverload - public init( - _ store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - @ViewBuilder content: @escaping ( - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) { - self.init( - store, - state: toDestinationState, - id: { $0.id }, - action: fromDestinationAction, - content: content - ) - } - - fileprivate init( - _ store: Store, PresentationAction>, - id toID: @escaping (PresentationState) -> ID?, - content: @escaping ( - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) where State == DestinationState, Action == DestinationAction { - let store = store.scope( - id: store.id(state: \.self, action: \.self), - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0.wrappedValue == nil } - ) - let viewStore = ViewStore( - store, - observe: { $0 }, - removeDuplicates: { toID($0) == toID($1) } - ) - - self.store = store - self.toDestinationState = { $0 } - self.toID = toID - self.fromDestinationAction = { $0 } - self.destinationStore = store.scope( - id: store.id(state: \.wrappedValue, action: \.presented), - state: ToState(\.wrappedValue), - action: { .presented($0) }, - isInvalid: nil - ) - self.content = content - self.viewStore = viewStore - } - - fileprivate init( - _ store: Store, PresentationAction>, - state toDestinationState: @escaping (State) -> DestinationState?, - id toID: @escaping (PresentationState) -> ID?, - action fromDestinationAction: @escaping (DestinationAction) -> Action, - content: @escaping ( - _ item: Binding, - _ destination: DestinationContent - ) -> Content - ) { - let store = store.scope( - id: nil, - state: ToState(\.self), - action: { $0 }, - isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } - ) - let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { toID($0) == toID($1) }) - - self.store = store - self.toDestinationState = toDestinationState - self.toID = toID - self.fromDestinationAction = fromDestinationAction - self.destinationStore = store.scope( - id: nil, - state: ToState { $0.wrappedValue.flatMap(toDestinationState) }, - action: { .presented(fromDestinationAction($0)) }, - isInvalid: nil - ) - self.content = content - self.viewStore = viewStore - } - - public var body: some View { - let id = self.toID(self.viewStore.state) - self.content( - self.viewStore.binding( - get: { - $0.wrappedValue.flatMap(toDestinationState) != nil - ? toID($0).map { AnyIdentifiable(Identified($0) { $0 }) } - : nil - }, - compactSend: { [weak viewStore = self.viewStore] in - guard - let viewStore = viewStore, - $0 == nil, - viewStore.wrappedValue != nil, - id == nil || self.toID(viewStore.state) == id - else { return nil } - return .dismiss - } - ), - DestinationContent(store: self.destinationStore) - ) - } -} - -@_spi(Presentation) -public struct AnyIdentifiable: Identifiable { - public let id: AnyHashable - - public init(_ base: Base) { - self.id = base.id - } -} - -#if swift(<5.10) - @MainActor(unsafe) -#else - @preconcurrency@MainActor -#endif -@_spi(Presentation) -public struct DestinationContent { - let store: Store - - public func callAsFunction( - @ViewBuilder _ body: @escaping (_ store: Store) -> Content - ) -> some View { - IfLetStore( - self.store.scope( - id: self.store.id(state: \.self, action: \.self), - state: ToState(returningLastNonNilValue { $0 }), - action: { $0 }, - isInvalid: nil - ), - then: body - ) - } -} +//import SwiftUI +// +//extension View { +// @_spi(Presentation) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation( +// store: Store, PresentationAction>, +// @ViewBuilder body: @escaping ( +// _ content: Self, +// _ isPresented: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) -> some View { +// self.presentation(store: store) { `self`, $item, destination in +// body(self, Binding($item), destination) +// } +// } +// +// @_disfavoredOverload +// @_spi(Presentation) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation( +// store: Store, PresentationAction>, +// @ViewBuilder body: @escaping ( +// _ content: Self, +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) -> some View { +// self.presentation( +// store: store, +// id: { $0.wrappedValue.map { _ in ObjectIdentifier(State.self) } } +// ) { `self`, $item, destination in +// body(self, $item, destination) +// } +// } +// +// @_disfavoredOverload +// @_spi(Presentation) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation( +// store: Store, PresentationAction>, +// id toID: @escaping (PresentationState) -> AnyHashable?, +// @ViewBuilder body: @escaping ( +// _ content: Self, +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) -> some View { +// PresentationStore(store, id: toID) { $item, destination in +// body(self, $item, destination) +// } +// } +// +// @_spi(Presentation) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation< +// State, +// Action, +// DestinationState, +// DestinationAction, +// Content: View +// >( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// @ViewBuilder body: @escaping ( +// _ content: Self, +// _ isPresented: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, destination in +// body(self, Binding($item), destination) +// } +// } +// +// @_disfavoredOverload +// @_spi(Presentation) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation< +// State, +// Action, +// DestinationState, +// DestinationAction, +// Content: View +// >( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// @ViewBuilder body: @escaping ( +// _ content: Self, +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) -> some View { +// self.presentation( +// store: store, +// state: toDestinationState, +// id: { $0.id }, +// action: fromDestinationAction, +// body: body +// ) +// } +// +// @_spi(Presentation) +// @ViewBuilder +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func presentation< +// State, +// Action, +// DestinationState, +// DestinationAction, +// Content: View +// >( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (State) -> DestinationState?, +// id toID: @escaping (PresentationState) -> AnyHashable?, +// action fromDestinationAction: @escaping (DestinationAction) -> Action, +// @ViewBuilder body: @escaping ( +// Self, +// Binding, +// DestinationContent +// ) -> Content +// ) -> some View { +// PresentationStore( +// store, state: toDestinationState, id: toID, action: fromDestinationAction +// ) { $item, destination in +// body(self, $item, destination) +// } +// } +//} +// +//@_spi(Presentation) +//public struct PresentationStore< +// State, Action, DestinationState, DestinationAction, Content: View +//>: View { +// let store: Store, PresentationAction> +// let toDestinationState: (State) -> DestinationState? +// let toID: (PresentationState) -> AnyHashable? +// let fromDestinationAction: (DestinationAction) -> Action +// let destinationStore: Store +// let content: +// ( +// Binding, +// DestinationContent +// ) -> Content +// +// @ObservedObject var viewStore: ViewStore, PresentationAction> +// +// public init( +// _ store: Store, PresentationAction>, +// @ViewBuilder content: @escaping ( +// _ isPresented: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) where State == DestinationState, Action == DestinationAction { +// self.init(store) { $item, destination in +// content(Binding($item), destination) +// } +// } +// +// @_disfavoredOverload +// public init( +// _ store: Store, PresentationAction>, +// @ViewBuilder content: @escaping ( +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) where State == DestinationState, Action == DestinationAction { +// self.init( +// store, +// id: { $0.id }, +// content: content +// ) +// } +// +// public init( +// _ store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// @ViewBuilder content: @escaping ( +// _ isPresented: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) { +// self.init( +// store, state: toDestinationState, action: fromDestinationAction +// ) { $item, destination in +// content(Binding($item), destination) +// } +// } +// +// @_disfavoredOverload +// public init( +// _ store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// @ViewBuilder content: @escaping ( +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) { +// self.init( +// store, +// state: toDestinationState, +// id: { $0.id }, +// action: fromDestinationAction, +// content: content +// ) +// } +// +// fileprivate init( +// _ store: Store, PresentationAction>, +// id toID: @escaping (PresentationState) -> ID?, +// content: @escaping ( +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) where State == DestinationState, Action == DestinationAction { +// let store = store.scope( +// id: store.id(state: \.self, action: \.self), +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0.wrappedValue == nil } +// ) +// let viewStore = ViewStore( +// store, +// observe: { $0 }, +// removeDuplicates: { toID($0) == toID($1) } +// ) +// +// self.store = store +// self.toDestinationState = { $0 } +// self.toID = toID +// self.fromDestinationAction = { $0 } +// self.destinationStore = store.scope( +// id: store.id(state: \.wrappedValue, action: \.presented), +// state: ToState(\.wrappedValue), +// action: { .presented($0) }, +// isInvalid: nil +// ) +// self.content = content +// self.viewStore = viewStore +// } +// +// fileprivate init( +// _ store: Store, PresentationAction>, +// state toDestinationState: @escaping (State) -> DestinationState?, +// id toID: @escaping (PresentationState) -> ID?, +// action fromDestinationAction: @escaping (DestinationAction) -> Action, +// content: @escaping ( +// _ item: Binding, +// _ destination: DestinationContent +// ) -> Content +// ) { +// let store = store.scope( +// id: nil, +// state: ToState(\.self), +// action: { $0 }, +// isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } +// ) +// let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { toID($0) == toID($1) }) +// +// self.store = store +// self.toDestinationState = toDestinationState +// self.toID = toID +// self.fromDestinationAction = fromDestinationAction +// self.destinationStore = store.scope( +// id: nil, +// state: ToState { $0.wrappedValue.flatMap(toDestinationState) }, +// action: { .presented(fromDestinationAction($0)) }, +// isInvalid: nil +// ) +// self.content = content +// self.viewStore = viewStore +// } +// +// public var body: some View { +// let id = self.toID(self.viewStore.state) +// self.content( +// self.viewStore.binding( +// get: { +// $0.wrappedValue.flatMap(toDestinationState) != nil +// ? toID($0).map { AnyIdentifiable(Identified($0) { $0 }) } +// : nil +// }, +// compactSend: { [weak viewStore = self.viewStore] in +// guard +// let viewStore = viewStore, +// $0 == nil, +// viewStore.wrappedValue != nil, +// id == nil || self.toID(viewStore.state) == id +// else { return nil } +// return .dismiss +// } +// ), +// DestinationContent(store: self.destinationStore) +// ) +// } +//} +// +//@_spi(Presentation) +//public struct AnyIdentifiable: Identifiable { +// public let id: AnyHashable +// +// public init(_ base: Base) { +// self.id = base.id +// } +//} +// +//#if swift(<5.10) +// @MainActor(unsafe) +//#else +// @preconcurrency@MainActor +//#endif +//@_spi(Presentation) +//public struct DestinationContent { +// let store: Store +// +// public func callAsFunction( +// @ViewBuilder _ body: @escaping (_ store: Store) -> Content +// ) -> some View { +// IfLetStore( +// self.store.scope( +// id: self.store.id(state: \.self, action: \.self), +// state: ToState(returningLastNonNilValue { $0 }), +// action: { $0 }, +// isInvalid: nil +// ), +// then: body +// ) +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/Sheet.swift b/Sources/ComposableArchitecture/SwiftUI/Sheet.swift index a7d161e9b487..e9522507c6a6 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Sheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Sheet.swift @@ -1,108 +1,108 @@ -import SwiftUI - -extension View { - /// Presents a sheet using the given store as a data source for the sheet's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a sheet - /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the - /// system dismisses the currently displayed sheet. - /// - onDismiss: The closure to execute when dismissing the modal view. - /// - content: A closure returning the content of the modal view. - @available( - iOS, deprecated: 9999, - message: - "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - macOS, deprecated: 9999, - message: - "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - tvOS, deprecated: 9999, - message: - "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - @available( - watchOS, deprecated: 9999, - message: - "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func sheet( - store: Store, PresentationAction>, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (_ store: Store) -> Content - ) -> some View { - self.presentation(store: store) { `self`, $item, destination in - self.sheet(item: $item, onDismiss: onDismiss) { _ in - destination(content) - } - } - } - - /// Presents a sheet using the given store as a data source for the sheet's content. - /// - /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. - /// - /// - Parameters: - /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for - /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` - /// and `Action` to the modifier's closure. You use this store to power the content in a sheet - /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the - /// system dismisses the currently displayed sheet. - /// - toDestinationState: A transformation to extract modal state from the presentation state. - /// - fromDestinationAction: A transformation to embed modal actions into the presentation - /// action. - /// - onDismiss: The closure to execute when dismissing the modal view. - /// - content: A closure returning the content of the modal view. - @available( - iOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - macOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - tvOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - @available( - watchOS, deprecated: 9999, - message: - "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" - ) - #if swift(<5.10) - @MainActor(unsafe) - #else - @preconcurrency@MainActor - #endif - public func sheet( - store: Store, PresentationAction>, - state toDestinationState: @escaping (_ state: State) -> DestinationState?, - action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, - onDismiss: (() -> Void)? = nil, - @ViewBuilder content: @escaping (_ store: Store) -> Content - ) -> some View { - self.presentation( - store: store, state: toDestinationState, action: fromDestinationAction - ) { `self`, $item, destination in - self.sheet(item: $item, onDismiss: onDismiss) { _ in - destination(content) - } - } - } -} +//import SwiftUI +// +//extension View { +// /// Presents a sheet using the given store as a data source for the sheet's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet +// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the +// /// system dismisses the currently displayed sheet. +// /// - onDismiss: The closure to execute when dismissing the modal view. +// /// - content: A closure returning the content of the modal view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func sheet( +// store: Store, PresentationAction>, +// onDismiss: (() -> Void)? = nil, +// @ViewBuilder content: @escaping (_ store: Store) -> Content +// ) -> some View { +// self.presentation(store: store) { `self`, $item, destination in +// self.sheet(item: $item, onDismiss: onDismiss) { _ in +// destination(content) +// } +// } +// } +// +// /// Presents a sheet using the given store as a data source for the sheet's content. +// /// +// /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. +// /// +// /// - Parameters: +// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for +// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` +// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet +// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the +// /// system dismisses the currently displayed sheet. +// /// - toDestinationState: A transformation to extract modal state from the presentation state. +// /// - fromDestinationAction: A transformation to embed modal actions into the presentation +// /// action. +// /// - onDismiss: The closure to execute when dismissing the modal view. +// /// - content: A closure returning the content of the modal view. +// @available( +// iOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// macOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// tvOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// @available( +// watchOS, deprecated: 9999, +// message: +// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" +// ) +// #if swift(<5.10) +// @MainActor(unsafe) +// #else +// @preconcurrency@MainActor +// #endif +// public func sheet( +// store: Store, PresentationAction>, +// state toDestinationState: @escaping (_ state: State) -> DestinationState?, +// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, +// onDismiss: (() -> Void)? = nil, +// @ViewBuilder content: @escaping (_ store: Store) -> Content +// ) -> some View { +// self.presentation( +// store: store, state: toDestinationState, action: fromDestinationAction +// ) { `self`, $item, destination in +// self.sheet(item: $item, onDismiss: onDismiss) { _ in +// destination(content) +// } +// } +// } +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift index 72a78401ca17..8bce1b7465d1 100644 --- a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift @@ -1,261 +1,256 @@ -@_spi(Reflection) import CasePaths -import SwiftUI - -/// A view that observes when enum state held in a store changes cases, and provides stores to -/// ``CaseLet`` views. -/// -/// An application may model parts of its state with enums. For example, app state may differ if a -/// user is logged-in or not: -/// -/// ```swift -/// @Reducer -/// struct AppFeature { -/// enum State { -/// case loggedIn(LoggedInState) -/// case loggedOut(LoggedOutState) -/// } -/// // ... -/// } -/// ``` -/// -/// In the view layer, a store on this state can switch over each case using a ``SwitchStore`` and -/// a ``CaseLet`` view per case: -/// -/// ```swift -/// struct AppView: View { -/// let store: StoreOf -/// -/// var body: some View { -/// SwitchStore(self.store) { state in -/// switch state { -/// case .loggedIn: -/// CaseLet( -/// /AppFeature.State.loggedIn, action: AppFeature.Action.loggedIn -/// ) { loggedInStore in -/// LoggedInView(store: loggedInStore) -/// } -/// case .loggedOut: -/// CaseLet( -/// /AppFeature.State.loggedOut, action: AppFeature.Action.loggedOut -/// ) { loggedOutStore in -/// LoggedOutView(store: loggedOutStore) -/// } -/// } -/// } -/// } -/// } -/// ``` -/// -/// > Important: The `SwitchStore` view builder is only evaluated when the case of state passed to -/// > it changes. As such, you should not rely on this value for anything other than checking the -/// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`. -/// -/// See ``Reducer/ifCaseLet(_:action:then:fileID:filePath:line:column:)-7sg8d`` and -/// ``Scope/init(state:action:child:fileID:filePath:line:column:)-9g44g`` for embedding reducers -/// that operate on each case of an enum in reducers that operate on the entire enum. -@available( - iOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - macOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - tvOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - watchOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -public struct SwitchStore: View { - public let store: Store - public let content: (State) -> Content - - public init( - _ store: Store, - @ViewBuilder content: @escaping (_ initialState: State) -> Content - ) { - self.store = store - self.content = content - } - - public var body: some View { - WithViewStore( - self.store, observe: { $0 }, removeDuplicates: { enumTag($0) == enumTag($1) } - ) { viewStore in - self.content(viewStore.state) - .environmentObject(StoreObservableObject(store: self.store)) - } - } -} - -/// A view that handles a specific case of enum state in a ``SwitchStore``. -@available( - iOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - macOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - tvOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -@available( - watchOS, deprecated: 9999, - message: - "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -) -public struct CaseLet: View { - public let toCaseState: (EnumState) -> CaseState? - public let fromCaseAction: (CaseAction) -> EnumAction - public let content: (Store) -> Content - - private let fileID: StaticString - private let filePath: StaticString - private let line: UInt - private let column: UInt - - @EnvironmentObject private var store: StoreObservableObject - - /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state - /// matches a particular case. - /// - /// - Parameters: - /// - toCaseState: A function that can extract a case of switch store state, which can be - /// specified using case path literal syntax, _e.g._ `/State.case`. - /// - fromCaseAction: A function that can embed a case action in a switch store action. - /// - content: A function that is given a store of the given case's state and returns a view - /// that is visible only when the switch store's state matches. - public init( - _ toCaseState: @escaping (EnumState) -> CaseState?, - action fromCaseAction: @escaping (CaseAction) -> EnumAction, - @ViewBuilder then content: @escaping (_ store: Store) -> Content, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) { - self.toCaseState = toCaseState - self.fromCaseAction = fromCaseAction - self.content = content - self.fileID = fileID - self.filePath = filePath - self.line = line - self.column = column - } - - public var body: some View { - IfLetStore( - self.store.wrappedValue.scope( - id: nil, - state: ToState(self.toCaseState), - action: self.fromCaseAction, - isInvalid: nil - ), - then: self.content, - else: { - _CaseLetMismatchView( - fileID: self.fileID, - filePath: self.filePath, - line: self.line, - column: self.column - ) - } - ) - } -} - -extension CaseLet where EnumAction == CaseAction { - /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state - /// matches a particular case. - /// - /// - Parameters: - /// - toCaseState: A function that can extract a case of switch store state, which can be - /// specified using case path literal syntax, _e.g._ `/State.case`. - /// - content: A function that is given a store of the given case's state and returns a view - /// that is visible only when the switch store's state matches. - public init( - state toCaseState: @escaping (EnumState) -> CaseState?, - @ViewBuilder then content: @escaping (_ store: Store) -> Content - ) { - self.init( - toCaseState, - action: { $0 }, - then: content - ) - } -} - -public struct _CaseLetMismatchView: View { - @EnvironmentObject private var store: StoreObservableObject - let fileID: StaticString - let filePath: StaticString - let line: UInt - let column: UInt - - public var body: some View { - #if DEBUG - let message = """ - Warning: A "CaseLet" at "\(self.fileID):\(self.line)" was encountered when state was set \ - to another case: - - \(debugCaseOutput(self.store.wrappedValue.withState { $0 })) - - This usually happens when there is a mismatch between the case being switched on and the \ - "CaseLet" view being rendered. - - For example, if ".screenA" is being switched on, but the "CaseLet" view is pointed to \ - ".screenB": - - case .screenA: - CaseLet( - /State.screenB, action: Action.screenB - ) { /* ... */ } - - Look out for typos to ensure that these two cases align. - """ - return VStack(spacing: 17) { - #if os(macOS) - Text("⚠️") - #else - Image(systemName: "exclamationmark.triangle.fill") - .font(.largeTitle) - #endif - - Text(message) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .foregroundColor(.white) - .padding() - .background(Color.red.edgesIgnoringSafeArea(.all)) - .onAppear { - reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column) - } - #else - return EmptyView() - #endif - } -} - -private final class StoreObservableObject: ObservableObject { - let wrappedValue: Store - - init(store: Store) { - self.wrappedValue = store - } -} - -private func enumTag(_ `case`: Case) -> UInt32? { - EnumMetadata(Case.self)?.tag(of: `case`) -} +//@_spi(Reflection) import CasePaths +//import SwiftUI +// +///// A view that observes when enum state held in a store changes cases, and provides stores to +///// ``CaseLet`` views. +///// +///// An application may model parts of its state with enums. For example, app state may differ if a +///// user is logged-in or not: +///// +///// ```swift +///// @Reducer +///// struct AppFeature { +///// enum State { +///// case loggedIn(LoggedInState) +///// case loggedOut(LoggedOutState) +///// } +///// // ... +///// } +///// ``` +///// +///// In the view layer, a store on this state can switch over each case using a ``SwitchStore`` and +///// a ``CaseLet`` view per case: +///// +///// ```swift +///// struct AppView: View { +///// let store: StoreOf +///// +///// var body: some View { +///// SwitchStore(self.store) { state in +///// switch state { +///// case .loggedIn: +///// CaseLet( +///// /AppFeature.State.loggedIn, action: AppFeature.Action.loggedIn +///// ) { loggedInStore in +///// LoggedInView(store: loggedInStore) +///// } +///// case .loggedOut: +///// CaseLet( +///// /AppFeature.State.loggedOut, action: AppFeature.Action.loggedOut +///// ) { loggedOutStore in +///// LoggedOutView(store: loggedOutStore) +///// } +///// } +///// } +///// } +///// } +///// ``` +///// +///// > Important: The `SwitchStore` view builder is only evaluated when the case of state passed to +///// > it changes. As such, you should not rely on this value for anything other than checking the +///// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`. +///// +///// See ``Reducer/ifCaseLet(_:action:then:fileID:filePath:line:column:)-7sg8d`` and +///// ``Scope/init(state:action:child:fileID:filePath:line:column:)-9g44g`` for embedding reducers +///// that operate on each case of an enum in reducers that operate on the entire enum. +//@available( +// iOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// macOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// tvOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// watchOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//public struct SwitchStore: View { +// public let store: Store +// public let content: (State) -> Content +// +// public init( +// _ store: Store, +// @ViewBuilder content: @escaping (_ initialState: State) -> Content +// ) { +// self.store = store +// self.content = content +// } +// +// public var body: some View { +// WithViewStore( +// self.store, observe: { $0 }, removeDuplicates: { enumTag($0) == enumTag($1) } +// ) { viewStore in +// self.content(viewStore.state) +// .environmentObject(StoreObservableObject(store: self.store)) +// } +// } +//} +// +///// A view that handles a specific case of enum state in a ``SwitchStore``. +//@available( +// iOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// macOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// tvOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//@available( +// watchOS, deprecated: 9999, +// message: +// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +//) +//public struct CaseLet: View { +// public let toCaseState: (EnumState) -> CaseState? +// public let fromCaseAction: (CaseAction) -> EnumAction +// public let content: (Store) -> Content +// +// private let fileID: StaticString +// private let filePath: StaticString +// private let line: UInt +// private let column: UInt +// +// @EnvironmentObject private var store: StoreObservableObject +// +// /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state +// /// matches a particular case. +// /// +// /// - Parameters: +// /// - toCaseState: A function that can extract a case of switch store state, which can be +// /// specified using case path literal syntax, _e.g._ `/State.case`. +// /// - fromCaseAction: A function that can embed a case action in a switch store action. +// /// - content: A function that is given a store of the given case's state and returns a view +// /// that is visible only when the switch store's state matches. +// public init( +// _ toCaseState: @escaping (EnumState) -> CaseState?, +// action fromCaseAction: @escaping (CaseAction) -> EnumAction, +// @ViewBuilder then content: @escaping (_ store: Store) -> Content, +// fileID: StaticString = #fileID, +// filePath: StaticString = #filePath, +// line: UInt = #line, +// column: UInt = #column +// ) { +// self.toCaseState = toCaseState +// self.fromCaseAction = fromCaseAction +// self.content = content +// self.fileID = fileID +// self.filePath = filePath +// self.line = line +// self.column = column +// } +// +// public var body: some View { +// IfLetStore( +// self.store.wrappedValue._scope(state: self.toCaseState, action: self.fromCaseAction), +// then: self.content, +// else: { +// _CaseLetMismatchView( +// fileID: self.fileID, +// filePath: self.filePath, +// line: self.line, +// column: self.column +// ) +// } +// ) +// } +//} +// +//extension CaseLet where EnumAction == CaseAction { +// /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state +// /// matches a particular case. +// /// +// /// - Parameters: +// /// - toCaseState: A function that can extract a case of switch store state, which can be +// /// specified using case path literal syntax, _e.g._ `/State.case`. +// /// - content: A function that is given a store of the given case's state and returns a view +// /// that is visible only when the switch store's state matches. +// public init( +// state toCaseState: @escaping (EnumState) -> CaseState?, +// @ViewBuilder then content: @escaping (_ store: Store) -> Content +// ) { +// self.init( +// toCaseState, +// action: { $0 }, +// then: content +// ) +// } +//} +// +//public struct _CaseLetMismatchView: View { +// @EnvironmentObject private var store: StoreObservableObject +// let fileID: StaticString +// let filePath: StaticString +// let line: UInt +// let column: UInt +// +// public var body: some View { +// #if DEBUG +// let message = """ +// Warning: A "CaseLet" at "\(self.fileID):\(self.line)" was encountered when state was set \ +// to another case: +// +// \(debugCaseOutput(self.store.wrappedValue.withState { $0 })) +// +// This usually happens when there is a mismatch between the case being switched on and the \ +// "CaseLet" view being rendered. +// +// For example, if ".screenA" is being switched on, but the "CaseLet" view is pointed to \ +// ".screenB": +// +// case .screenA: +// CaseLet( +// /State.screenB, action: Action.screenB +// ) { /* ... */ } +// +// Look out for typos to ensure that these two cases align. +// """ +// return VStack(spacing: 17) { +// #if os(macOS) +// Text("⚠️") +// #else +// Image(systemName: "exclamationmark.triangle.fill") +// .font(.largeTitle) +// #endif +// +// Text(message) +// } +// .frame(maxWidth: .infinity, maxHeight: .infinity) +// .foregroundColor(.white) +// .padding() +// .background(Color.red.edgesIgnoringSafeArea(.all)) +// .onAppear { +// reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column) +// } +// #else +// return EmptyView() +// #endif +// } +//} +// +//private final class StoreObservableObject: ObservableObject { +// let wrappedValue: Store +// +// init(store: Store) { +// self.wrappedValue = store +// } +//} +// +//private func enumTag(_ `case`: Case) -> UInt32? { +// EnumMetadata(Case.self)?.tag(of: `case`) +//} diff --git a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift index 3ed5b2f6098d..cf215086cd6d 100644 --- a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift @@ -491,12 +491,7 @@ public struct WithViewStore: View { line: UInt = #line ) { self.init( - store: store.scope( - id: nil, - state: ToState(toViewState), - action: fromViewAction, - isInvalid: nil - ), + store: store._scope(state: toViewState, action: fromViewAction), removeDuplicates: isDuplicate, content: content, file: file, @@ -585,12 +580,7 @@ public struct WithViewStore: View { line: UInt = #line ) { self.init( - store: store.scope( - id: nil, - state: ToState(toViewState), - action: { $0 }, - isInvalid: nil - ), + store: store._scope(state: toViewState, action: { $0 }), removeDuplicates: isDuplicate, content: content, file: file, @@ -680,12 +670,7 @@ extension WithViewStore where ViewState: Equatable, Content: View { line: UInt = #line ) { self.init( - store: store.scope( - id: nil, - state: ToState(toViewState), - action: fromViewAction, - isInvalid: nil - ), + store: store._scope(state: toViewState, action: fromViewAction), removeDuplicates: ==, content: content, file: file, @@ -771,12 +756,7 @@ extension WithViewStore where ViewState: Equatable, Content: View { line: UInt = #line ) { self.init( - store: store.scope( - id: nil, - state: ToState(toViewState), - action: { $0 }, - isInvalid: nil - ), + store: store._scope(state: toViewState, action: { $0 }), removeDuplicates: ==, content: content, file: file, diff --git a/Sources/ComposableArchitecture/TestStore.swift b/Sources/ComposableArchitecture/TestStore.swift index a26eafeb50eb..0360e60528d3 100644 --- a/Sources/ComposableArchitecture/TestStore.swift +++ b/Sources/ComposableArchitecture/TestStore.swift @@ -967,12 +967,11 @@ extension TestStore where State: Equatable { let expectedState = self.state let previousState = self.reducer.state let previousStackElementID = self.reducer.dependencies.stackElementID.incrementingCopy() - let task = self.sharedChangeTracker.track { + let task: Task? = self.sharedChangeTracker.track { self.store.send( .init( origin: .send(action), fileID: fileID, filePath: filePath, line: line, column: column - ), - originatingFrom: nil + ) ) } if uncheckedUseMainSerialExecutor { @@ -2549,12 +2548,7 @@ extension TestStore { store: Store(initialState: self.state) { BindingReducer(action: toViewAction.extract(from:)) } - .scope( - id: nil, - state: ToState(\.self), - action: toViewAction.embed, - isInvalid: nil - ) + ._scope(state: { $0 }, action: toViewAction.embed) ) } } diff --git a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift index 93dda83a0cbb..a693a8f5e566 100644 --- a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift @@ -52,18 +52,21 @@ extension Store { return self .publisher .removeDuplicates(by: { ($0 != nil) == ($1 != nil) }) - .sink { state in - if var state = state { - unwrap( - self.scope( - id: self.id(state: \.!, action: \.self), - state: ToState { - state = $0 ?? state - return state - }, - action: { $0 }, - isInvalid: { $0 == nil } + .sink { [weak self] state in + if let self, let state { + @MainActor + func open(_ core: some Core) -> Store { + Store( + core: IfLetCore( + base: core, + cachedState: state, + stateKeyPath: \.self, + actionKeyPath: \.self + ) ) + } + unwrap( + open(self.core) ) } else { `else`() diff --git a/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift b/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift index 8db0e6510757..7a265f205d4f 100644 --- a/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift @@ -44,26 +44,43 @@ root: root ) navigationDestination(for: StackState.Component.self) { component in - var element = component.element - return destination( - path.wrappedValue.scope( - id: path.wrappedValue.id( - state: - \.[ - id:component.id,fileID:_HashableStaticString( - rawValue: fileID),filePath:_HashableStaticString( - rawValue: filePath),line:line,column:column - ], - action: \.[id:component.id] - ), - state: ToState { - element = $0[id: component.id] ?? element - return element - }, - action: { .element(id: component.id, action: $0) }, - isInvalid: { !$0.ids.contains(component.id) } - ) + let id = path.wrappedValue.id( + state: + \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + action: \.[id:component.id] ) + guard let child = path.wrappedValue.children[id] as? Store + else { + @MainActor + func open( + _ core: some Core, StackAction> + ) -> UIViewController { + let child = Store( + core: IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) + ) + path.wrappedValue.children[id] = child + return destination(child) + } + return open(path.wrappedValue.core) + } + return destination(child) } } } diff --git a/Sources/ComposableArchitecture/ViewStore.swift b/Sources/ComposableArchitecture/ViewStore.swift index 064231780fe1..6fbb362698df 100644 --- a/Sources/ComposableArchitecture/ViewStore.swift +++ b/Sources/ComposableArchitecture/ViewStore.swift @@ -155,14 +155,9 @@ public final class ViewStore: ObservableObject { self.storeTypeName = ComposableArchitecture.storeTypeName(of: store) Logger.shared.log("View\(self.storeTypeName).init") #endif - self.store = store.scope( - id: nil, - state: ToState(toViewState), - action: fromViewAction, - isInvalid: nil - ) + self.store = store._scope(state: toViewState, action: fromViewAction) self._state = CurrentValueRelay(self.store.withState { $0 }) - self.viewCancellable = self.store.rootStore.didSet + self.viewCancellable = self.store.core.didSet .compactMap { [weak self] in self?.store.withState { $0 } } .removeDuplicates(by: isDuplicate) .dropFirst() From 3fe6fb08208cdda06113676eab454fb65fdd5f88 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:00:48 -0700 Subject: [PATCH 02/26] wip --- Sources/ComposableArchitecture/Store.swift | 41 ---------------------- 1 file changed, 41 deletions(-) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 0ef110d8a43a..a347573ef6cd 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -590,47 +590,6 @@ func typeName( return name } -@_spi(Internals) -public struct ToState { - fileprivate let base: PartialToState - @_spi(Internals) - public init(_ closure: @escaping (State) -> ChildState) { - self.base = .closure { closure($0 as! State) } - } - @_spi(Internals) - public init(_ keyPath: KeyPath) { - self.base = .keyPath(keyPath) - } -} - -private enum PartialToState { - case closure((Any) -> State) - case keyPath(AnyKeyPath) - case appended((Any) -> Any, AnyKeyPath) - func callAsFunction(_ state: Any) -> State { - switch self { - case let .closure(closure): - return closure(state) - case let .keyPath(keyPath): - return state[keyPath: keyPath] as! State - case let .appended(closure, keyPath): - return closure(state)[keyPath: keyPath] as! State - } - } - func appending(_ state: PartialToState) -> PartialToState { - switch (self, state) { - case let (.keyPath(lhs), .keyPath(rhs)): - return .keyPath(lhs.appending(path: rhs)!) - case let (.closure(lhs), .keyPath(rhs)): - return .appended(lhs, rhs) - case let (.appended(lhsClosure, lhsKeyPath), .keyPath(rhs)): - return .appended(lhsClosure, lhsKeyPath.appending(path: rhs)!) - default: - return .closure { state(self($0)) } - } - } -} - let _isStorePerceptionCheckingEnabled: Bool = { if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { return false From 9122a3aaaec478914bcc02f9e03187f4aaa1fe18 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:01:10 -0700 Subject: [PATCH 03/26] wip --- Sources/ComposableArchitecture/Store.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index a347573ef6cd..753751845d1e 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -503,10 +503,6 @@ public struct StoreTask: Hashable, Sendable { } } -private protocol _OptionalProtocol {} -extension Optional: _OptionalProtocol {} -extension PresentationState: _OptionalProtocol {} - func storeTypeName(of store: Store) -> String { let stateType = typeName(State.self, genericsAbbreviated: false) let actionType = typeName(Action.self, genericsAbbreviated: false) From f80cb3b51c3a35d4bd1d90724f543fddec03e32a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:07:58 -0700 Subject: [PATCH 04/26] wip --- Sources/ComposableArchitecture/Core.swift | 18 +++++++++++++++-- .../Observation/Binding+Observation.swift | 20 +++++++++++++++---- .../Observation/Store+Observation.swift | 2 +- Sources/ComposableArchitecture/Store.swift | 2 -- .../SwiftUI/Binding.swift | 4 ++-- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index 9f7b1d14a91d..8888c734dbd2 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -9,6 +9,7 @@ protocol Core: AnyObject, Sendable { func send(_ action: Action) -> Task? var didSet: CurrentValueRelay { get } + var isInvalid: Bool { get } } final class InvalidCore: Core { @@ -19,6 +20,7 @@ final class InvalidCore: Core { func send(_ action: Action) -> Task? { nil } let didSet = CurrentValueRelay(()) + let isInvalid = true } final class RootCore: Core { @@ -28,9 +30,12 @@ final class RootCore: Core { } } let reducer: Root - var bufferedActions: [Root.Action] = [] + let didSet = CurrentValueRelay(()) - var effectCancellables: [UUID: AnyCancellable] = [:] + let isInvalid = false + + private var bufferedActions: [Root.Action] = [] + private var effectCancellables: [UUID: AnyCancellable] = [:] private var isSending = false init( initialState: Root.State, @@ -192,6 +197,9 @@ class ScopedCore: Core { var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { + base.isInvalid + } } class IfLetCore: Core { @@ -224,6 +232,9 @@ class IfLetCore: Core { var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { + base.state[keyPath: stateKeyPath] == nil || base.isInvalid + } } class ClosureScopedCore: Core { @@ -248,4 +259,7 @@ class ClosureScopedCore: Core { var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { + base.isInvalid + } } diff --git a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift index 7f5fcdb4e8ea..21ca223d15c8 100644 --- a/Sources/ComposableArchitecture/Observation/Binding+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Binding+Observation.swift @@ -164,7 +164,9 @@ extension Store where State: ObservableState, Action: BindableAction, Action.Sta get { self.state[keyPath: keyPath] } set { BindingLocal.$isActive.withValue(true) { - self.send(.set(keyPath, newValue, isInvalidated: _isInvalidated)) + self.send( + .set(keyPath, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true }) + ) } } } @@ -181,7 +183,9 @@ where get { self.observableState } set { BindingLocal.$isActive.withValue(true) { - self.send(.set(\.self, newValue, isInvalidated: _isInvalidated)) + self.send( + .set(\.self, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true }) + ) } } } @@ -200,7 +204,11 @@ where get { self.state[keyPath: keyPath] } set { BindingLocal.$isActive.withValue(true) { - self.send(.view(.set(keyPath, newValue, isInvalidated: _isInvalidated))) + self.send( + .view( + .set(keyPath, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true }) + ) + ) } } } @@ -218,7 +226,11 @@ where get { self.observableState } set { BindingLocal.$isActive.withValue(true) { - self.send(.view(.set(\.self, newValue, isInvalidated: _isInvalidated))) + self.send( + .view( + .set(\.self, newValue, isInvalidated: { [weak self] in self?.core.isInvalid ?? true }) + ) + ) } } } diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index bab58739a422..0d5edf81c594 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -412,7 +412,7 @@ extension Store where State: ObservableState { if newValue == nil, let childState = self.state[keyPath: state], id == _identifiableID(childState), - !self._isInvalidated() + !self.core.isInvalid { self.send(action(.dismiss)) if self.state[keyPath: state] != nil { diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 753751845d1e..6bd99d3ceeba 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -140,7 +140,6 @@ import SwiftUI public final class Store { var canCacheChildren = true var children: [ScopeID: AnyObject] = [:] - var _isInvalidated: @MainActor @Sendable () -> Bool = { false } let core: any Core @@ -176,7 +175,6 @@ public final class Store { } init() { - self._isInvalidated = { true } self.core = InvalidCore() } diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift index 47e5e0c8f1f4..715bc15cc0d6 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -310,7 +310,7 @@ extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewSt value: value, bindableActionType: ViewAction.self, context: .bindingState, - isInvalidated: self.store._isInvalidated, + isInvalidated: { [weak self] in self?.store.core.isInvalid ?? true }, fileID: bindingState.fileID, filePath: bindingState.filePath, line: bindingState.line, @@ -460,7 +460,7 @@ public struct BindingViewStore { value: value, bindableActionType: self.bindableActionType, context: .bindingStore, - isInvalidated: self.store._isInvalidated, + isInvalidated: { [weak store] in store?.core.isInvalid ?? true }, fileID: self.fileID, filePath: self.filePath, line: self.line, From f67562a9d0e329958090c79f51efe99e041c0543 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:43:58 -0700 Subject: [PATCH 05/26] wip --- Sources/ComposableArchitecture/Core.swift | 16 ++++++- .../IdentifiedArray+Observation.swift | 20 ++++----- .../NavigationStack+Observation.swift | 42 ++++++++---------- .../Observation/Store+Observation.swift | 26 ++++------- Sources/ComposableArchitecture/Store.swift | 43 +++++++++++-------- .../UIKit/IfLetUIKit.swift | 18 +++----- .../NavigationStackControllerUIKit.swift | 42 ++++++++---------- 7 files changed, 97 insertions(+), 110 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index 8888c734dbd2..c296b7686118 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -8,6 +8,7 @@ protocol Core: AnyObject, Sendable { var state: State { get } func send(_ action: Action) -> Task? + var canStoreCacheChildren: Bool { get } var didSet: CurrentValueRelay { get } var isInvalid: Bool { get } } @@ -19,8 +20,9 @@ final class InvalidCore: Core { } func send(_ action: Action) -> Task? { nil } + var canStoreCacheChildren: Bool { false } let didSet = CurrentValueRelay(()) - let isInvalid = true + var isInvalid: Bool { true } } final class RootCore: Core { @@ -31,8 +33,9 @@ final class RootCore: Core { } let reducer: Root + var canStoreCacheChildren: Bool { true } let didSet = CurrentValueRelay(()) - let isInvalid = false + var isInvalid: Bool { false } private var bufferedActions: [Root.Action] = [] private var effectCancellables: [UUID: AnyCancellable] = [:] @@ -194,6 +197,9 @@ class ScopedCore: Core { func send(_ action: Action) -> Task? { base.send(actionKeyPath(action)) } + var canStoreCacheChildren: Bool { + base.canStoreCacheChildren + } var didSet: CurrentValueRelay { base.didSet } @@ -229,6 +235,9 @@ class IfLetCore: Core { #endif return base.send(actionKeyPath(action)) } + var canStoreCacheChildren: Bool { + base.canStoreCacheChildren + } var didSet: CurrentValueRelay { base.didSet } @@ -256,6 +265,9 @@ class ClosureScopedCore: Core { func send(_ action: Action) -> Task? { base.send(fromAction(action)) } + var canStoreCacheChildren: Bool { + false + } var didSet: CurrentValueRelay { base.didSet } diff --git a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift index 5fcb47fbc048..9dfea36209b1 100644 --- a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift @@ -72,7 +72,7 @@ extension Store where State: ObservableState { line: UInt = #line, column: UInt = #column ) -> some RandomAccessCollection> { - if !self.canCacheChildren { + if !core.canStoreCacheChildren { reportIssue( uncachedStoreWarning(self), fileID: fileID, @@ -128,19 +128,15 @@ public struct _StoreCollection: RandomAc @MainActor func open( _ core: some Core, IdentifiedAction> - ) -> Store { - let child = Store( - core: IfLetCore( - base: core, - cachedState: self.data[position], - stateKeyPath: \.[id:elementID], - actionKeyPath: \.[id:elementID] - ) + ) -> any Core { + IfLetCore( + base: core, + cachedState: self.data[position], + stateKeyPath: \.[id:elementID], + actionKeyPath: \.[id:elementID] ) - self.store.children[scopeID] = child - return child } - return open(self.store.core) + return self.store.scope(id: scopeID, childCore: open(self.store.core)) } return child } diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index 7dd747dcc0e5..a9ade7c60014 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -204,32 +204,24 @@ public struct _NavigationDestinationViewModifier< ], action: \.[id:component.id] ) - if let child = store.children[id] as? Store { - return destination(child) - } else { - @MainActor - func open( - _ core: some Core, StackAction> - ) -> Destination { - let child = Store( - core: IfLetCore( - base: core, - cachedState: component.element, - stateKeyPath: \.[ - id:component.id, - fileID:_HashableStaticString(rawValue: fileID), - filePath:_HashableStaticString(rawValue: filePath), - line:line, - column:column - ], - actionKeyPath: \.[id:component.id] - ) - ) - store.children[id] = child - return destination(child) - } - return open(store.core) + @MainActor + func open( + _ core: some Core, StackAction> + ) -> any Core { + IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) } + return destination(store.scope(id: id, childCore: open(store.core))) } } diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index 0d5edf81c594..6edae8f923de 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -88,7 +88,7 @@ extension Store where State: ObservableState { line: UInt = #line, column: UInt = #column ) -> Store? { - if !canCacheChildren { + if !core.canStoreCacheChildren { reportIssue( uncachedStoreWarning(self), fileID: fileID, @@ -103,23 +103,15 @@ extension Store where State: ObservableState { children[id] = nil // TODO: Eager? return nil } - guard let child = children[id] as? Store - else { - func open(_ core: some Core) -> Store { - let child = Store( - core: IfLetCore( - base: core, - cachedState: childState, - stateKeyPath: stateKeyPath, - actionKeyPath: actionKeyPath - ) - ) - children[id] = child - return child - } - return open(core) + func open(_ core: some Core) -> any Core { + IfLetCore( + base: core, + cachedState: childState, + stateKeyPath: stateKeyPath, + actionKeyPath: actionKeyPath + ) } - return child + return scope(id: id, childCore: open(core)) } } diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 6bd99d3ceeba..780967a9215c 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -138,7 +138,6 @@ import SwiftUI @preconcurrency@MainActor #endif public final class Store { - var canCacheChildren = true var children: [ScopeID: AnyObject] = [:] let core: any Core @@ -294,16 +293,26 @@ public final class Store { state: KeyPath, action: CaseKeyPath ) -> Store { - let id = ScopeID(state: state, action: action) - guard let child = children[id] as? Store else { - func open(_ core: some Core) -> Store { - let child = Store( - core: ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) - ) + func open(_ core: some Core) -> any Core { + ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) + } + return scope(id: ScopeID(state: state, action: action), childCore: open(core)) + } + + func scope( + id: ScopeID?, + childCore: @autoclosure () -> any Core + ) -> Store { + guard + core.canStoreCacheChildren, + let id, + let child = children[id] as? Store + else { + let child = Store(core: childCore()) + if core.canStoreCacheChildren, let id { children[id] = child - return child } - return open(core) + return child } return child } @@ -324,16 +333,14 @@ public final class Store { state toChildState: @escaping (_ state: State) -> ChildState, action fromChildAction: @escaping (_ childAction: ChildAction) -> Action ) -> Store { - func open(_ core: some Core) -> Store { - Store( - core: ClosureScopedCore( - base: core, - toState: toChildState, - fromAction: fromChildAction - ) + func open(_ core: some Core) -> any Core { + ClosureScopedCore( + base: core, + toState: toChildState, + fromAction: fromChildAction ) } - return open(core) + return scope(id: nil, childCore: open(core)) } @_spi(Internals) @@ -347,7 +354,7 @@ public final class Store { core.send(action) } - init(core: some Core) { + private init(core: some Core) { defer { Logger.shared.log("\(storeTypeName(of: self)).init") } self.core = core let didSet = core.didSet diff --git a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift index a693a8f5e566..879f1ce5cae4 100644 --- a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift @@ -55,19 +55,15 @@ extension Store { .sink { [weak self] state in if let self, let state { @MainActor - func open(_ core: some Core) -> Store { - Store( - core: IfLetCore( - base: core, - cachedState: state, - stateKeyPath: \.self, - actionKeyPath: \.self - ) + func open(_ core: some Core) -> any Core { + IfLetCore( + base: core, + cachedState: state, + stateKeyPath: \.self, + actionKeyPath: \.self ) } - unwrap( - open(self.core) - ) + unwrap(self.scope(id: nil, childCore: open(self.core))) } else { `else`() } diff --git a/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift b/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift index 7a265f205d4f..e7445e72be78 100644 --- a/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/NavigationStackControllerUIKit.swift @@ -55,32 +55,24 @@ ], action: \.[id:component.id] ) - guard let child = path.wrappedValue.children[id] as? Store - else { - @MainActor - func open( - _ core: some Core, StackAction> - ) -> UIViewController { - let child = Store( - core: IfLetCore( - base: core, - cachedState: component.element, - stateKeyPath: \.[ - id:component.id, - fileID:_HashableStaticString(rawValue: fileID), - filePath:_HashableStaticString(rawValue: filePath), - line:line, - column:column - ], - actionKeyPath: \.[id:component.id] - ) - ) - path.wrappedValue.children[id] = child - return destination(child) - } - return open(path.wrappedValue.core) + @MainActor + func open( + _ core: some Core, StackAction> + ) -> any Core { + IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) } - return destination(child) + return destination(path.wrappedValue.scope(id: id, childCore: open(path.wrappedValue.core))) } } } From 2bd4f44a87bb595640dcace77cd1bac1d52bec80 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:48:54 -0700 Subject: [PATCH 06/26] wip --- Sources/ComposableArchitecture/Store.swift | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 780967a9215c..57abbab10ec9 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -357,22 +357,19 @@ public final class Store { private init(core: some Core) { defer { Logger.shared.log("\(storeTypeName(of: self)).init") } self.core = core - let didSet = core.didSet - - if State.self is any ObservableState.Type { - @Sendable - func onChange() { - withPerceptionTracking { - MainActor.assumeIsolated { _ = core.state } - } onChange: { [weak self] in - guard let self else { return } - MainActor.assumeIsolated { + + if let stateType = State.self as? any ObservableState.Type { + func subscribeToDidSet(_ type: T.Type) -> AnyCancellable { + core.didSet + .compactMap { [weak self] in (self?.currentState as? T)?._$id } + .removeDuplicates() + .dropFirst() + .sink { [weak self] _ in + guard let self else { return } self._$observationRegistrar.withMutation(of: self, keyPath: \.currentState) {} } - didSet.send(()) - onChange() - } } + self.parentCancellable = subscribeToDidSet(stateType) } } From 00ad75ea02a9bcfd0af46f640ed8d000e4ff7f87 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 13:50:51 -0700 Subject: [PATCH 07/26] wip --- .../Observation/IdentifiedArray+Observation.swift | 4 +--- .../Observation/Store+Observation.swift | 2 +- Sources/ComposableArchitecture/Store.swift | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift index 9dfea36209b1..e69784940850 100644 --- a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift @@ -120,9 +120,7 @@ public struct _StoreCollection: RandomAc return Store() } let elementID = self.data.ids[position] - let scopeID = ScopeID, IdentifiedAction>( - state: \.[id:elementID], action: \.[id:elementID] - ) + let scopeID = self.store.id(state: \.[id:elementID], action: \.[id:elementID]) guard let child = self.store.children[scopeID] as? Store else { @MainActor diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index 6edae8f923de..f5c7f4192486 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -97,7 +97,7 @@ extension Store where State: ObservableState { column: column ) } - let id = ScopeID(state: stateKeyPath, action: actionKeyPath) + let id = id(state: stateKeyPath, action: actionKeyPath) guard let childState = state[keyPath: stateKeyPath] else { children[id] = nil // TODO: Eager? diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 57abbab10ec9..5cd59b9eabd5 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -296,7 +296,7 @@ public final class Store { func open(_ core: some Core) -> any Core { ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) } - return scope(id: ScopeID(state: state, action: action), childCore: open(core)) + return scope(id: id(state: state, action: action), childCore: open(core)) } func scope( From 65c42e1d7ad9f6bdc2ca34fa085f39d086d9b8c8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:34:26 -0700 Subject: [PATCH 08/26] wip --- .../SwiftUI/IfLetStore.swift | 624 +++++++-------- .../SwiftUI/PresentationModifier.swift | 737 +++++++++--------- 2 files changed, 695 insertions(+), 666 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift index 35f1dadf1b73..35832b5462b7 100644 --- a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift @@ -1,306 +1,318 @@ -//import SwiftUI -// -///// A view that safely unwraps a store of optional state in order to show one of two views. -///// -///// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` -///// that holds onto non-optional state, and otherwise the `else` closure will be performed. -///// -///// This is useful for deciding between two views to show depending on an optional piece of state: -///// -///// ```swift -///// IfLetStore( -///// store.scope(state: \.results, action: { .results($0) }) -///// ) { -///// SearchResultsView(store: $0) -///// } else: { -///// Text("Loading search results...") -///// } -///// ``` -///// -//@available( -// iOS, deprecated: 9999, -// message: -// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -//) -//@available( -// macOS, deprecated: 9999, -// message: -// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -//) -//@available( -// tvOS, deprecated: 9999, -// message: -// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -//) -//@available( -// watchOS, deprecated: 9999, -// message: -// "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" -//) -//public struct IfLetStore: View { -// private let content: (ViewStore) -> Content -// private let store: Store -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional -// /// state is `nil` or non-`nil`. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil`. -// /// - elseContent: A view that is only visible when the optional state is `nil`. -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, -// @ViewBuilder else elseContent: () -> ElseContent -// ) where Content == _ConditionalContent { -// let store = store.scope( -// id: store.id(state: \.self, action: \.self), -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0 == nil } -// ) -// self.store = store -// let elseContent = elseContent() -// self.content = { viewStore in -// if var state = viewStore.state { -// return ViewBuilder.buildEither( -// first: ifContent( -// store.scope( -// id: store.id(state: \.!, action: \.self), -// state: ToState { -// state = $0 ?? state -// return state -// }, -// action: { $0 }, -// isInvalid: { $0 == nil } -// ) -// ) -// ) -// } else { -// return ViewBuilder.buildEither(second: elseContent) -// } -// } -// } -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional -// /// state is `nil` or non-`nil`. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil`. -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent -// ) where Content == IfContent? { -// let store = store.scope( -// id: store.id(state: \.self, action: \.self), -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0 == nil } -// ) -// self.store = store -// self.content = { viewStore in -// if var state = viewStore.state { -// return ifContent( -// store.scope( -// id: store.id(state: \.!, action: \.self), -// state: ToState { -// state = $0 ?? state -// return state -// }, -// action: { $0 }, -// isInvalid: { $0 == nil } -// ) -// ) -// } else { -// return nil -// } -// } -// } -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of -// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil`. -// /// - elseContent: A view that is only visible when the optional state is `nil`. -// @available( -// iOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, PresentationAction>, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, -// @ViewBuilder else elseContent: @escaping () -> ElseContent -// ) where Content == _ConditionalContent { -// self.init( -// store.scope(state: \.wrappedValue, action: \.presented), -// then: ifContent, -// else: elseContent -// ) -// } -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of -// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil`. -// @available( -// iOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, PresentationAction>, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent -// ) where Content == IfContent? { -// self.init( -// store.scope(state: \.wrappedValue, action: \.presented), -// then: ifContent -// ) -// } -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of -// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further -// /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - toState: A closure that attempts to extract state for the "if" branch from the destination -// /// state. -// /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil` and state can be extracted from the -// /// destination state. -// /// - elseContent: A view that is only visible when state cannot be extracted from the -// /// destination. -// @available( -// *, deprecated, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, PresentationAction>, -// state toState: @escaping (_ destinationState: DestinationState) -> State?, -// action fromAction: @escaping (_ action: Action) -> DestinationAction, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, -// @ViewBuilder else elseContent: @escaping () -> ElseContent -// ) where Content == _ConditionalContent { -// self.init( -// store.scope( -// state: { $0.wrappedValue.flatMap(toState) }, -// action: { .presented(fromAction($0)) } -// ), -// then: ifContent, -// else: elseContent -// ) -// } -// -// /// Initializes an ``IfLetStore`` view that computes content depending on if a store of -// /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further -// /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. -// /// -// /// - Parameters: -// /// - store: A store of optional state. -// /// - toState: A closure that attempts to extract state for the "if" branch from the destination -// /// state. -// /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. -// /// - ifContent: A function that is given a store of non-optional state and returns a view that -// /// is visible only when the optional state is non-`nil` and state can be extracted from the -// /// destination state. -// @available( -// *, deprecated, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, PresentationAction>, -// state toState: @escaping (_ destinationState: DestinationState) -> State?, -// action fromAction: @escaping (_ action: Action) -> DestinationAction, -// @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent -// ) where Content == IfContent? { -// self.init( -// store.scope( -// state: { $0.wrappedValue.flatMap(toState) }, -// action: { .presented(fromAction($0)) } -// ), -// then: ifContent -// ) -// } -// -// public var body: some View { -// WithViewStore( -// self.store, -// observe: { $0 }, -// removeDuplicates: { ($0 != nil) == ($1 != nil) }, -// content: self.content -// ) -// } -//} +import SwiftUI + +/// A view that safely unwraps a store of optional state in order to show one of two views. +/// +/// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` +/// that holds onto non-optional state, and otherwise the `else` closure will be performed. +/// +/// This is useful for deciding between two views to show depending on an optional piece of state: +/// +/// ```swift +/// IfLetStore( +/// store.scope(state: \.results, action: { .results($0) }) +/// ) { +/// SearchResultsView(store: $0) +/// } else: { +/// Text("Loading search results...") +/// } +/// ``` +/// +@available( + iOS, deprecated: 9999, + message: + "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +) +@available( + macOS, deprecated: 9999, + message: + "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +) +@available( + tvOS, deprecated: 9999, + message: + "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +) +@available( + watchOS, deprecated: 9999, + message: + "Use 'if let' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]" +) +public struct IfLetStore: View { + private let content: (ViewStore) -> Content + private let store: Store + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional + /// state is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + /// - elseContent: A view that is only visible when the optional state is `nil`. + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, + @ViewBuilder else elseContent: () -> ElseContent + ) where Content == _ConditionalContent { + func open(_ core: some Core) -> any Core { + _IfLetCore(base: core) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + self.store = store + let elseContent = elseContent() + self.content = { viewStore in + if let state = viewStore.state { + @MainActor + func open(_ core: some Core) -> any Core { + IfLetCore(base: core, cachedState: state, stateKeyPath: \.self, actionKeyPath: \.self) + } + return ViewBuilder.buildEither( + first: ifContent( + store.scope( + id: store.id(state: \.!, action: \.self), + childCore: open(store.core) + ) + ) + ) + } else { + return ViewBuilder.buildEither(second: elseContent) + } + } + } + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional + /// state is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent + ) where Content == IfContent? { + func open(_ core: some Core) -> any Core { + _IfLetCore(base: core) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + self.store = store + self.content = { viewStore in + if let state = viewStore.state { + @MainActor + func open(_ core: some Core) -> any Core { + IfLetCore(base: core, cachedState: state, stateKeyPath: \.self, actionKeyPath: \.self) + } + return ifContent( + store.scope( + id: store.id(state: \.!, action: \.self), + childCore: open(store.core) + ) + ) + } else { + return nil + } + } + } + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of + /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + /// - elseContent: A view that is only visible when the optional state is `nil`. + @available( + iOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, PresentationAction>, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, + @ViewBuilder else elseContent: @escaping () -> ElseContent + ) where Content == _ConditionalContent { + self.init( + store.scope(state: \.wrappedValue, action: \.presented), + then: ifContent, + else: elseContent + ) + } + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of + /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + @available( + iOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Scope the store into the destination's wrapped 'state' and presented 'action', instead: 'store.scope(state: \\.destination, action: \\.destination.presented)'. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, PresentationAction>, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent + ) where Content == IfContent? { + self.init( + store.scope(state: \.wrappedValue, action: \.presented), + then: ifContent + ) + } + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of + /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further + /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - toState: A closure that attempts to extract state for the "if" branch from the destination + /// state. + /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil` and state can be extracted from the + /// destination state. + /// - elseContent: A view that is only visible when state cannot be extracted from the + /// destination. + @available( + *, deprecated, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, PresentationAction>, + state toState: @escaping (_ destinationState: DestinationState) -> State?, + action fromAction: @escaping (_ action: Action) -> DestinationAction, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent, + @ViewBuilder else elseContent: @escaping () -> ElseContent + ) where Content == _ConditionalContent { + self.init( + store.scope( + state: { $0.wrappedValue.flatMap(toState) }, + action: { .presented(fromAction($0)) } + ), + then: ifContent, + else: elseContent + ) + } + + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of + /// ``PresentationState`` and ``PresentationAction`` is `nil` or non-`nil` and state can further + /// be extracted from the destination state, _e.g._ it matches a particular case of an enum. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - toState: A closure that attempts to extract state for the "if" branch from the destination + /// state. + /// - fromAction: A closure that embeds actions for the "if" branch in destination actions. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil` and state can be extracted from the + /// destination state. + @available( + *, deprecated, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, PresentationAction>, + state toState: @escaping (_ destinationState: DestinationState) -> State?, + action fromAction: @escaping (_ action: Action) -> DestinationAction, + @ViewBuilder then ifContent: @escaping (_ store: Store) -> IfContent + ) where Content == IfContent? { + self.init( + store.scope( + state: { $0.wrappedValue.flatMap(toState) }, + action: { .presented(fromAction($0)) } + ), + then: ifContent + ) + } + + public var body: some View { + WithViewStore( + self.store, + observe: { $0 }, + removeDuplicates: { ($0 != nil) == ($1 != nil) }, + content: self.content + ) + } +} + +private final class _IfLetCore, Wrapped, Action>: Core { + let base: Base + init(base: Base) { + self.base = base + } + var state: Base.State { base.state } + func send(_ action: Action) -> Task? { base.send(action) } + var canStoreCacheChildren: Bool { base.canStoreCacheChildren } + var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { state == nil || base.isInvalid } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift index 1ba4b920bb61..6b98a058a7f5 100644 --- a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift +++ b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift @@ -1,360 +1,377 @@ -//import SwiftUI -// -//extension View { -// @_spi(Presentation) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation( -// store: Store, PresentationAction>, -// @ViewBuilder body: @escaping ( -// _ content: Self, -// _ isPresented: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) -> some View { -// self.presentation(store: store) { `self`, $item, destination in -// body(self, Binding($item), destination) -// } -// } -// -// @_disfavoredOverload -// @_spi(Presentation) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation( -// store: Store, PresentationAction>, -// @ViewBuilder body: @escaping ( -// _ content: Self, -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) -> some View { -// self.presentation( -// store: store, -// id: { $0.wrappedValue.map { _ in ObjectIdentifier(State.self) } } -// ) { `self`, $item, destination in -// body(self, $item, destination) -// } -// } -// -// @_disfavoredOverload -// @_spi(Presentation) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation( -// store: Store, PresentationAction>, -// id toID: @escaping (PresentationState) -> AnyHashable?, -// @ViewBuilder body: @escaping ( -// _ content: Self, -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) -> some View { -// PresentationStore(store, id: toID) { $item, destination in -// body(self, $item, destination) -// } -// } -// -// @_spi(Presentation) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation< -// State, -// Action, -// DestinationState, -// DestinationAction, -// Content: View -// >( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// @ViewBuilder body: @escaping ( -// _ content: Self, -// _ isPresented: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, destination in -// body(self, Binding($item), destination) -// } -// } -// -// @_disfavoredOverload -// @_spi(Presentation) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation< -// State, -// Action, -// DestinationState, -// DestinationAction, -// Content: View -// >( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// @ViewBuilder body: @escaping ( -// _ content: Self, -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) -> some View { -// self.presentation( -// store: store, -// state: toDestinationState, -// id: { $0.id }, -// action: fromDestinationAction, -// body: body -// ) -// } -// -// @_spi(Presentation) -// @ViewBuilder -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func presentation< -// State, -// Action, -// DestinationState, -// DestinationAction, -// Content: View -// >( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (State) -> DestinationState?, -// id toID: @escaping (PresentationState) -> AnyHashable?, -// action fromDestinationAction: @escaping (DestinationAction) -> Action, -// @ViewBuilder body: @escaping ( -// Self, -// Binding, -// DestinationContent -// ) -> Content -// ) -> some View { -// PresentationStore( -// store, state: toDestinationState, id: toID, action: fromDestinationAction -// ) { $item, destination in -// body(self, $item, destination) -// } -// } -//} -// -//@_spi(Presentation) -//public struct PresentationStore< -// State, Action, DestinationState, DestinationAction, Content: View -//>: View { -// let store: Store, PresentationAction> -// let toDestinationState: (State) -> DestinationState? -// let toID: (PresentationState) -> AnyHashable? -// let fromDestinationAction: (DestinationAction) -> Action -// let destinationStore: Store -// let content: -// ( -// Binding, -// DestinationContent -// ) -> Content -// -// @ObservedObject var viewStore: ViewStore, PresentationAction> -// -// public init( -// _ store: Store, PresentationAction>, -// @ViewBuilder content: @escaping ( -// _ isPresented: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) where State == DestinationState, Action == DestinationAction { -// self.init(store) { $item, destination in -// content(Binding($item), destination) -// } -// } -// -// @_disfavoredOverload -// public init( -// _ store: Store, PresentationAction>, -// @ViewBuilder content: @escaping ( -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) where State == DestinationState, Action == DestinationAction { -// self.init( -// store, -// id: { $0.id }, -// content: content -// ) -// } -// -// public init( -// _ store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// @ViewBuilder content: @escaping ( -// _ isPresented: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) { -// self.init( -// store, state: toDestinationState, action: fromDestinationAction -// ) { $item, destination in -// content(Binding($item), destination) -// } -// } -// -// @_disfavoredOverload -// public init( -// _ store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// @ViewBuilder content: @escaping ( -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) { -// self.init( -// store, -// state: toDestinationState, -// id: { $0.id }, -// action: fromDestinationAction, -// content: content -// ) -// } -// -// fileprivate init( -// _ store: Store, PresentationAction>, -// id toID: @escaping (PresentationState) -> ID?, -// content: @escaping ( -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) where State == DestinationState, Action == DestinationAction { -// let store = store.scope( -// id: store.id(state: \.self, action: \.self), -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0.wrappedValue == nil } -// ) -// let viewStore = ViewStore( -// store, -// observe: { $0 }, -// removeDuplicates: { toID($0) == toID($1) } -// ) -// -// self.store = store -// self.toDestinationState = { $0 } -// self.toID = toID -// self.fromDestinationAction = { $0 } -// self.destinationStore = store.scope( -// id: store.id(state: \.wrappedValue, action: \.presented), -// state: ToState(\.wrappedValue), -// action: { .presented($0) }, -// isInvalid: nil -// ) -// self.content = content -// self.viewStore = viewStore -// } -// -// fileprivate init( -// _ store: Store, PresentationAction>, -// state toDestinationState: @escaping (State) -> DestinationState?, -// id toID: @escaping (PresentationState) -> ID?, -// action fromDestinationAction: @escaping (DestinationAction) -> Action, -// content: @escaping ( -// _ item: Binding, -// _ destination: DestinationContent -// ) -> Content -// ) { -// let store = store.scope( -// id: nil, -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } -// ) -// let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { toID($0) == toID($1) }) -// -// self.store = store -// self.toDestinationState = toDestinationState -// self.toID = toID -// self.fromDestinationAction = fromDestinationAction -// self.destinationStore = store.scope( -// id: nil, -// state: ToState { $0.wrappedValue.flatMap(toDestinationState) }, -// action: { .presented(fromDestinationAction($0)) }, -// isInvalid: nil -// ) -// self.content = content -// self.viewStore = viewStore -// } -// -// public var body: some View { -// let id = self.toID(self.viewStore.state) -// self.content( -// self.viewStore.binding( -// get: { -// $0.wrappedValue.flatMap(toDestinationState) != nil -// ? toID($0).map { AnyIdentifiable(Identified($0) { $0 }) } -// : nil -// }, -// compactSend: { [weak viewStore = self.viewStore] in -// guard -// let viewStore = viewStore, -// $0 == nil, -// viewStore.wrappedValue != nil, -// id == nil || self.toID(viewStore.state) == id -// else { return nil } -// return .dismiss -// } -// ), -// DestinationContent(store: self.destinationStore) -// ) -// } -//} -// -//@_spi(Presentation) -//public struct AnyIdentifiable: Identifiable { -// public let id: AnyHashable -// -// public init(_ base: Base) { -// self.id = base.id -// } -//} -// -//#if swift(<5.10) -// @MainActor(unsafe) -//#else -// @preconcurrency@MainActor -//#endif -//@_spi(Presentation) -//public struct DestinationContent { -// let store: Store -// -// public func callAsFunction( -// @ViewBuilder _ body: @escaping (_ store: Store) -> Content -// ) -> some View { -// IfLetStore( -// self.store.scope( -// id: self.store.id(state: \.self, action: \.self), -// state: ToState(returningLastNonNilValue { $0 }), -// action: { $0 }, -// isInvalid: nil -// ), -// then: body -// ) -// } -//} +import SwiftUI + +extension View { + @_spi(Presentation) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation( + store: Store, PresentationAction>, + @ViewBuilder body: @escaping ( + _ content: Self, + _ isPresented: Binding, + _ destination: DestinationContent + ) -> Content + ) -> some View { + self.presentation(store: store) { `self`, $item, destination in + body(self, Binding($item), destination) + } + } + + @_disfavoredOverload + @_spi(Presentation) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation( + store: Store, PresentationAction>, + @ViewBuilder body: @escaping ( + _ content: Self, + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) -> some View { + self.presentation( + store: store, + id: { $0.wrappedValue.map { _ in ObjectIdentifier(State.self) } } + ) { `self`, $item, destination in + body(self, $item, destination) + } + } + + @_disfavoredOverload + @_spi(Presentation) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation( + store: Store, PresentationAction>, + id toID: @escaping (PresentationState) -> AnyHashable?, + @ViewBuilder body: @escaping ( + _ content: Self, + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) -> some View { + PresentationStore(store, id: toID) { $item, destination in + body(self, $item, destination) + } + } + + @_spi(Presentation) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation< + State, + Action, + DestinationState, + DestinationAction, + Content: View + >( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + @ViewBuilder body: @escaping ( + _ content: Self, + _ isPresented: Binding, + _ destination: DestinationContent + ) -> Content + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, destination in + body(self, Binding($item), destination) + } + } + + @_disfavoredOverload + @_spi(Presentation) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation< + State, + Action, + DestinationState, + DestinationAction, + Content: View + >( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + @ViewBuilder body: @escaping ( + _ content: Self, + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) -> some View { + self.presentation( + store: store, + state: toDestinationState, + id: { $0.id }, + action: fromDestinationAction, + body: body + ) + } + + @_spi(Presentation) + @ViewBuilder + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func presentation< + State, + Action, + DestinationState, + DestinationAction, + Content: View + >( + store: Store, PresentationAction>, + state toDestinationState: @escaping (State) -> DestinationState?, + id toID: @escaping (PresentationState) -> AnyHashable?, + action fromDestinationAction: @escaping (DestinationAction) -> Action, + @ViewBuilder body: @escaping ( + Self, + Binding, + DestinationContent + ) -> Content + ) -> some View { + PresentationStore( + store, state: toDestinationState, id: toID, action: fromDestinationAction + ) { $item, destination in + body(self, $item, destination) + } + } +} + +@_spi(Presentation) +public struct PresentationStore< + State, Action, DestinationState, DestinationAction, Content: View +>: View { + let store: Store, PresentationAction> + let toDestinationState: (State) -> DestinationState? + let toID: (PresentationState) -> AnyHashable? + let fromDestinationAction: (DestinationAction) -> Action + let destinationStore: Store + let content: + ( + Binding, + DestinationContent + ) -> Content + + @ObservedObject var viewStore: ViewStore, PresentationAction> + + public init( + _ store: Store, PresentationAction>, + @ViewBuilder content: @escaping ( + _ isPresented: Binding, + _ destination: DestinationContent + ) -> Content + ) where State == DestinationState, Action == DestinationAction { + self.init(store) { $item, destination in + content(Binding($item), destination) + } + } + + @_disfavoredOverload + public init( + _ store: Store, PresentationAction>, + @ViewBuilder content: @escaping ( + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) where State == DestinationState, Action == DestinationAction { + self.init( + store, + id: { $0.id }, + content: content + ) + } + + public init( + _ store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + @ViewBuilder content: @escaping ( + _ isPresented: Binding, + _ destination: DestinationContent + ) -> Content + ) { + self.init( + store, state: toDestinationState, action: fromDestinationAction + ) { $item, destination in + content(Binding($item), destination) + } + } + + @_disfavoredOverload + public init( + _ store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + @ViewBuilder content: @escaping ( + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) { + self.init( + store, + state: toDestinationState, + id: { $0.id }, + action: fromDestinationAction, + content: content + ) + } + + fileprivate init( + _ store: Store, PresentationAction>, + id toID: @escaping (PresentationState) -> ID?, + content: @escaping ( + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) where State == DestinationState, Action == DestinationAction { + func open( + _ core: some Core, PresentationAction> + ) -> any Core, PresentationAction> { + PresentationCore(base: core, toDestinationState: { $0 }) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + let viewStore = ViewStore( + store, + observe: { $0 }, + removeDuplicates: { toID($0) == toID($1) } + ) + + self.store = store + self.toDestinationState = { $0 } + self.toID = toID + self.fromDestinationAction = { $0 } + self.destinationStore = store.scope(state: \.wrappedValue, action: \.presented) + self.content = content + self.viewStore = viewStore + } + + fileprivate init( + _ store: Store, PresentationAction>, + state toDestinationState: @escaping (State) -> DestinationState?, + id toID: @escaping (PresentationState) -> ID?, + action fromDestinationAction: @escaping (DestinationAction) -> Action, + content: @escaping ( + _ item: Binding, + _ destination: DestinationContent + ) -> Content + ) { + func open( + _ core: some Core, PresentationAction> + ) -> any Core, PresentationAction> { + PresentationCore(base: core, toDestinationState: toDestinationState) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + let viewStore = ViewStore(store, observe: { $0 }, removeDuplicates: { toID($0) == toID($1) }) + + self.store = store + self.toDestinationState = toDestinationState + self.toID = toID + self.fromDestinationAction = fromDestinationAction + self.destinationStore = store._scope( + state: { $0.wrappedValue.flatMap(toDestinationState) }, + action: { .presented(fromDestinationAction($0)) } + ) + self.content = content + self.viewStore = viewStore + } + + public var body: some View { + let id = self.toID(self.viewStore.state) + self.content( + self.viewStore.binding( + get: { + $0.wrappedValue.flatMap(toDestinationState) != nil + ? toID($0).map { AnyIdentifiable(Identified($0) { $0 }) } + : nil + }, + compactSend: { [weak viewStore = self.viewStore] in + guard + let viewStore = viewStore, + $0 == nil, + viewStore.wrappedValue != nil, + id == nil || self.toID(viewStore.state) == id + else { return nil } + return .dismiss + } + ), + DestinationContent(store: self.destinationStore) + ) + } +} + +private final class PresentationCore< + Base: Core, PresentationAction>, + State, + Action, + DestinationState +>: Core { + let base: Base + let toDestinationState: (State) -> DestinationState? + init( + base: Base, + toDestinationState: @escaping (State) -> DestinationState? + ) { + self.base = base + self.toDestinationState = toDestinationState + } + var state: Base.State { + base.state + } + func send(_ action: Base.Action) -> Task? { + base.send(action) + } + var canStoreCacheChildren: Bool { base.canStoreCacheChildren } + var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { state.wrappedValue.flatMap(toDestinationState) == nil || base.isInvalid } +} + +@_spi(Presentation) +public struct AnyIdentifiable: Identifiable { + public let id: AnyHashable + + public init(_ base: Base) { + self.id = base.id + } +} + +#if swift(<5.10) + @MainActor(unsafe) +#else + @preconcurrency@MainActor +#endif +@_spi(Presentation) +public struct DestinationContent { + let store: Store + + public func callAsFunction( + @ViewBuilder _ body: @escaping (_ store: Store) -> Content + ) -> some View { + IfLetStore(self.store, then: body) + } +} From e79668ef9646d71aa11e403b3e1d00d7ef40bc0a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:35:19 -0700 Subject: [PATCH 09/26] wip --- .../SwiftUI/Alert.swift | 212 ++++---- .../SwiftUI/ConfirmationDialog.swift | 222 ++++---- .../SwiftUI/NavigationDestination.swift | 260 ++++----- .../SwiftUI/Popover.swift | 232 ++++---- .../SwiftUI/Sheet.swift | 216 ++++---- .../SwiftUI/SwitchStore.swift | 512 +++++++++--------- 6 files changed, 827 insertions(+), 827 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 83cd68c85084..80b19f63cd96 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -1,106 +1,106 @@ -//import SwiftUI -// -//@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -//extension View { -// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes -// /// `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func alert( -// store: Store>, PresentationAction> -// ) -> some View { -// self._alert(store: store, state: { $0 }, action: { $0 }) -// } -// -// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes -// /// `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// /// - toDestinationState: A transformation to extract alert state from the presentation state. -// /// - fromDestinationAction: A transformation to embed alert actions into the presentation -// /// action. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func alert( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> AlertState?, -// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action -// ) -> some View { -// self._alert(store: store, state: toDestinationState, action: fromDestinationAction) -// } -// -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// private func _alert( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> AlertState?, -// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $isPresented, destination in -// let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } -// self.alert( -// (alertState?.title).map(Text.init) ?? Text(verbatim: ""), -// isPresented: $isPresented, -// presenting: alertState, -// actions: { alertState in -// ForEach(alertState.buttons) { button in -// Button(role: button.role.map(ButtonRole.init)) { -// switch button.action.type { -// case let .send(action): -// if let action { -// store.send(.presented(fromDestinationAction(action))) -// } -// case let .animatedSend(action, animation): -// if let action { -// store.send(.presented(fromDestinationAction(action)), animation: animation) -// } -// } -// } label: { -// Text(button.label) -// } -// } -// }, -// message: { -// $0.message.map(Text.init) -// } -// ) -// } -// } -//} +import SwiftUI + +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension View { + /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func alert( + store: Store>, PresentationAction> + ) -> some View { + self._alert(store: store, state: { $0 }, action: { $0 }) + } + + /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + /// - toDestinationState: A transformation to extract alert state from the presentation state. + /// - fromDestinationAction: A transformation to embed alert actions into the presentation + /// action. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func alert( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> AlertState?, + action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action + ) -> some View { + self._alert(store: store, state: toDestinationState, action: fromDestinationAction) + } + + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + private func _alert( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> AlertState?, + action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $isPresented, destination in + let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } + self.alert( + (alertState?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: $isPresented, + presenting: alertState, + actions: { alertState in + ForEach(alertState.buttons) { button in + Button(role: button.role.map(ButtonRole.init)) { + switch button.action.type { + case let .send(action): + if let action { + store.send(.presented(fromDestinationAction(action))) + } + case let .animatedSend(action, animation): + if let action { + store.send(.presented(fromDestinationAction(action)), animation: animation) + } + } + } label: { + Text(button.label) + } + } + }, + message: { + $0.message.map(Text.init) + } + ) + } + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift index 160898d4ec99..5f325fe746c7 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift @@ -1,111 +1,111 @@ -//import SwiftUI -// -//@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) -//extension View { -// /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes -// /// `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a -// /// dialog. -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func confirmationDialog( -// store: Store< -// PresentationState>, -// PresentationAction -// > -// ) -> some View { -// self._confirmationDialog(store: store, state: { $0 }, action: { $0 }) -// } -// -// /// Displays a dialog when then store's state becomes non-`nil`, and dismisses it when it becomes -// /// `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a -// /// dialog. -// /// - toDestinationState: A transformation to extract dialog state from the presentation state. -// /// - fromDestinationAction: A transformation to embed dialog actions into the presentation -// /// action. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func confirmationDialog( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, -// action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action -// ) -> some View { -// self._confirmationDialog(store: store, state: toDestinationState, action: fromDestinationAction) -// } -// -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// private func _confirmationDialog( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, -// action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $isPresented, destination in -// let confirmationDialogState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } -// self.confirmationDialog( -// (confirmationDialogState?.title).map(Text.init) ?? Text(verbatim: ""), -// isPresented: $isPresented, -// titleVisibility: (confirmationDialogState?.titleVisibility).map(Visibility.init) -// ?? .automatic, -// presenting: confirmationDialogState, -// actions: { confirmationDialogState in -// ForEach(confirmationDialogState.buttons) { button in -// Button(role: button.role.map(ButtonRole.init)) { -// switch button.action.type { -// case let .send(action): -// if let action { -// store.send(.presented(fromDestinationAction(action))) -// } -// case let .animatedSend(action, animation): -// if let action { -// store.send(.presented(fromDestinationAction(action)), animation: animation) -// } -// } -// } label: { -// Text(button.label) -// } -// } -// }, -// message: { -// $0.message.map(Text.init) -// } -// ) -// } -// } -//} +import SwiftUI + +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension View { + /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a + /// dialog. + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func confirmationDialog( + store: Store< + PresentationState>, + PresentationAction + > + ) -> some View { + self._confirmationDialog(store: store, state: { $0 }, action: { $0 }) + } + + /// Displays a dialog when then store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for a + /// dialog. + /// - toDestinationState: A transformation to extract dialog state from the presentation state. + /// - fromDestinationAction: A transformation to embed dialog actions into the presentation + /// action. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func confirmationDialog( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, + action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action + ) -> some View { + self._confirmationDialog(store: store, state: toDestinationState, action: fromDestinationAction) + } + + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + private func _confirmationDialog( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, + action fromDestinationAction: @escaping (_ confirmationDialogAction: ButtonAction) -> Action + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $isPresented, destination in + let confirmationDialogState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } + self.confirmationDialog( + (confirmationDialogState?.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: $isPresented, + titleVisibility: (confirmationDialogState?.titleVisibility).map(Visibility.init) + ?? .automatic, + presenting: confirmationDialogState, + actions: { confirmationDialogState in + ForEach(confirmationDialogState.buttons) { button in + Button(role: button.role.map(ButtonRole.init)) { + switch button.action.type { + case let .send(action): + if let action { + store.send(.presented(fromDestinationAction(action))) + } + case let .animatedSend(action, animation): + if let action { + store.send(.presented(fromDestinationAction(action)), animation: animation) + } + } + } label: { + Text(button.label) + } + } + }, + message: { + $0.message.map(Text.init) + } + ) + } + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift b/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift index 4bce897865c9..c819d0aab380 100644 --- a/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift +++ b/Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift @@ -1,130 +1,130 @@ -//@_spi(Reflection) import CasePaths -//import SwiftUI -// -//@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -//extension View { -// /// Associates a destination view with a store that can be used to push the view onto a -// /// `NavigationStack`. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's -// /// > `navigationDestination(isPresented:)` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped -// /// `State` and `Action` to the modifier's closure. You use this store to power the content -// /// in a view that the system pushes onto the navigation stack. If `store`'s state is -// /// `nil`-ed out, the system pops the view from the stack. -// /// - destination: A closure returning the content of the destination view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func navigationDestination( -// store: Store, PresentationAction>, -// @ViewBuilder destination: @escaping (_ store: Store) -> Destination -// ) -> some View { -// self.presentation( -// store: store, -// id: { $0.wrappedValue.map(NavigationDestinationID.init) } -// ) { `self`, $item, destinationContent in -// self.navigationDestination(isPresented: Binding($item)) { -// destinationContent(destination) -// } -// } -// } -// -// /// Associates a destination view with a store that can be used to push the view onto a -// /// `NavigationStack`. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's -// /// > `navigationDestination(isPresented:)` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped -// /// `State` and `Action` to the modifier's closure. You use this store to power the content -// /// in a view that the system pushes onto the navigation stack. If `store`'s state is -// /// `nil`-ed out, the system pops the view from the stack. -// /// - toDestinationState: A transformation to extract screen state from the presentation -// /// state. -// /// - fromDestinationAction: A transformation to embed screen actions into the presentation -// /// action. -// /// - destination: A closure returning the content of the destination view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func navigationDestination< -// State, Action, DestinationState, DestinationAction, Destination: View -// >( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// @ViewBuilder destination: @escaping (_ store: Store) -> -// Destination -// ) -> some View { -// self.presentation( -// store: store, -// state: toDestinationState, -// id: { $0.wrappedValue.map(NavigationDestinationID.init) }, -// action: fromDestinationAction -// ) { `self`, $item, destinationContent in -// self.navigationDestination(isPresented: Binding($item)) { -// destinationContent(destination) -// } -// } -// } -//} -// -//private struct NavigationDestinationID: Hashable { -// let objectIdentifier: ObjectIdentifier -// let enumTag: UInt32? -// -// init(_ value: Value) { -// self.objectIdentifier = ObjectIdentifier(Value.self) -// self.enumTag = EnumMetadata(Value.self)?.tag(of: value) -// } -//} +@_spi(Reflection) import CasePaths +import SwiftUI + +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +extension View { + /// Associates a destination view with a store that can be used to push the view onto a + /// `NavigationStack`. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's + /// > `navigationDestination(isPresented:)` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped + /// `State` and `Action` to the modifier's closure. You use this store to power the content + /// in a view that the system pushes onto the navigation stack. If `store`'s state is + /// `nil`-ed out, the system pops the view from the stack. + /// - destination: A closure returning the content of the destination view. + @available( + iOS, deprecated: 9999, + message: + "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + macOS, deprecated: 9999, + message: + "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + tvOS, deprecated: 9999, + message: + "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + watchOS, deprecated: 9999, + message: + "Pass a binding of a store to 'navigationDestination(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func navigationDestination( + store: Store, PresentationAction>, + @ViewBuilder destination: @escaping (_ store: Store) -> Destination + ) -> some View { + self.presentation( + store: store, + id: { $0.wrappedValue.map(NavigationDestinationID.init) } + ) { `self`, $item, destinationContent in + self.navigationDestination(isPresented: Binding($item)) { + destinationContent(destination) + } + } + } + + /// Associates a destination view with a store that can be used to push the view onto a + /// `NavigationStack`. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's + /// > `navigationDestination(isPresented:)` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a screen. When `store`'s state is non-`nil`, the system passes a store of unwrapped + /// `State` and `Action` to the modifier's closure. You use this store to power the content + /// in a view that the system pushes onto the navigation stack. If `store`'s state is + /// `nil`-ed out, the system pops the view from the stack. + /// - toDestinationState: A transformation to extract screen state from the presentation + /// state. + /// - fromDestinationAction: A transformation to embed screen actions into the presentation + /// action. + /// - destination: A closure returning the content of the destination view. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func navigationDestination< + State, Action, DestinationState, DestinationAction, Destination: View + >( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + @ViewBuilder destination: @escaping (_ store: Store) -> + Destination + ) -> some View { + self.presentation( + store: store, + state: toDestinationState, + id: { $0.wrappedValue.map(NavigationDestinationID.init) }, + action: fromDestinationAction + ) { `self`, $item, destinationContent in + self.navigationDestination(isPresented: Binding($item)) { + destinationContent(destination) + } + } + } +} + +private struct NavigationDestinationID: Hashable { + let objectIdentifier: ObjectIdentifier + let enumTag: UInt32? + + init(_ value: Value) { + self.objectIdentifier = ObjectIdentifier(Value.self) + self.enumTag = EnumMetadata(Value.self)?.tag(of: value) + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/Popover.swift b/Sources/ComposableArchitecture/SwiftUI/Popover.swift index e42640d770c7..b4caf45bd242 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Popover.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Popover.swift @@ -1,116 +1,116 @@ -//import SwiftUI -// -//@available(tvOS, unavailable) -//@available(watchOS, unavailable) -//extension View { -// /// Presents a popover using the given store as a data source for the popover's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a -// /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed -// /// out, the system dismisses the currently displayed popover. -// /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. -// /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's -// /// arrow in macOS. iOS ignores this parameter. -// /// - content: A closure returning the content of the popover. -// @available( -// iOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func popover( -// store: Store, PresentationAction>, -// attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), -// arrowEdge: Edge = .top, -// @ViewBuilder content: @escaping (_ store: Store) -> Content -// ) -> some View { -// self.presentation(store: store) { `self`, $item, destination in -// self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in -// destination(content) -// } -// } -// } -// -// /// Presents a popover using the given store as a data source for the popover's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a -// /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed -// /// out, the system dismisses the currently displayed popover. -// /// - toDestinationState: A transformation to extract popover state from the presentation state. -// /// - fromDestinationAction: A transformation to embed popover actions into the presentation -// /// action. -// /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. -// /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's -// /// arrow in macOS. iOS ignores this parameter. -// /// - content: A closure returning the content of the popover. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func popover( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), -// arrowEdge: Edge = .top, -// @ViewBuilder content: @escaping (_ store: Store) -> Content -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, destination in -// self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in -// destination(content) -// } -// } -// } -//} +import SwiftUI + +@available(tvOS, unavailable) +@available(watchOS, unavailable) +extension View { + /// Presents a popover using the given store as a data source for the popover's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a + /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed + /// out, the system dismisses the currently displayed popover. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow in macOS. iOS ignores this parameter. + /// - content: A closure returning the content of the popover. + @available( + iOS, deprecated: 9999, + message: + "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + macOS, deprecated: 9999, + message: + "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + tvOS, deprecated: 9999, + message: + "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + watchOS, deprecated: 9999, + message: + "Pass a binding of a store to 'popover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func popover( + store: Store, PresentationAction>, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (_ store: Store) -> Content + ) -> some View { + self.presentation(store: store) { `self`, $item, destination in + self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in + destination(content) + } + } + } + + /// Presents a popover using the given store as a data source for the popover's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `popover` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a + /// popover you create that the system displays to the user. If `store`'s state is `nil`-ed + /// out, the system dismisses the currently displayed popover. + /// - toDestinationState: A transformation to extract popover state from the presentation state. + /// - fromDestinationAction: A transformation to embed popover actions into the presentation + /// action. + /// - attachmentAnchor: The positioning anchor that defines the attachment point of the popover. + /// - arrowEdge: The edge of the `attachmentAnchor` that defines the location of the popover's + /// arrow in macOS. iOS ignores this parameter. + /// - content: A closure returning the content of the popover. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func popover( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), + arrowEdge: Edge = .top, + @ViewBuilder content: @escaping (_ store: Store) -> Content + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, destination in + self.popover(item: $item, attachmentAnchor: attachmentAnchor, arrowEdge: arrowEdge) { _ in + destination(content) + } + } + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/Sheet.swift b/Sources/ComposableArchitecture/SwiftUI/Sheet.swift index e9522507c6a6..a7d161e9b487 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Sheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Sheet.swift @@ -1,108 +1,108 @@ -//import SwiftUI -// -//extension View { -// /// Presents a sheet using the given store as a data source for the sheet's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet -// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the -// /// system dismisses the currently displayed sheet. -// /// - onDismiss: The closure to execute when dismissing the modal view. -// /// - content: A closure returning the content of the modal view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func sheet( -// store: Store, PresentationAction>, -// onDismiss: (() -> Void)? = nil, -// @ViewBuilder content: @escaping (_ store: Store) -> Content -// ) -> some View { -// self.presentation(store: store) { `self`, $item, destination in -// self.sheet(item: $item, onDismiss: onDismiss) { _ in -// destination(content) -// } -// } -// } -// -// /// Presents a sheet using the given store as a data source for the sheet's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet -// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the -// /// system dismisses the currently displayed sheet. -// /// - toDestinationState: A transformation to extract modal state from the presentation state. -// /// - fromDestinationAction: A transformation to embed modal actions into the presentation -// /// action. -// /// - onDismiss: The closure to execute when dismissing the modal view. -// /// - content: A closure returning the content of the modal view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func sheet( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// onDismiss: (() -> Void)? = nil, -// @ViewBuilder content: @escaping (_ store: Store) -> Content -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, destination in -// self.sheet(item: $item, onDismiss: onDismiss) { _ in -// destination(content) -// } -// } -// } -//} +import SwiftUI + +extension View { + /// Presents a sheet using the given store as a data source for the sheet's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a sheet + /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the + /// system dismisses the currently displayed sheet. + /// - onDismiss: The closure to execute when dismissing the modal view. + /// - content: A closure returning the content of the modal view. + @available( + iOS, deprecated: 9999, + message: + "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + macOS, deprecated: 9999, + message: + "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + tvOS, deprecated: 9999, + message: + "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + watchOS, deprecated: 9999, + message: + "Pass a binding of a store to 'sheet(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func sheet( + store: Store, PresentationAction>, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (_ store: Store) -> Content + ) -> some View { + self.presentation(store: store) { `self`, $item, destination in + self.sheet(item: $item, onDismiss: onDismiss) { _ in + destination(content) + } + } + } + + /// Presents a sheet using the given store as a data source for the sheet's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `sheet` view modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a sheet + /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the + /// system dismisses the currently displayed sheet. + /// - toDestinationState: A transformation to extract modal state from the presentation state. + /// - fromDestinationAction: A transformation to embed modal actions into the presentation + /// action. + /// - onDismiss: The closure to execute when dismissing the modal view. + /// - content: A closure returning the content of the modal view. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func sheet( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (_ store: Store) -> Content + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, destination in + self.sheet(item: $item, onDismiss: onDismiss) { _ in + destination(content) + } + } + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift index 8bce1b7465d1..026a89a69147 100644 --- a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift @@ -1,256 +1,256 @@ -//@_spi(Reflection) import CasePaths -//import SwiftUI -// -///// A view that observes when enum state held in a store changes cases, and provides stores to -///// ``CaseLet`` views. -///// -///// An application may model parts of its state with enums. For example, app state may differ if a -///// user is logged-in or not: -///// -///// ```swift -///// @Reducer -///// struct AppFeature { -///// enum State { -///// case loggedIn(LoggedInState) -///// case loggedOut(LoggedOutState) -///// } -///// // ... -///// } -///// ``` -///// -///// In the view layer, a store on this state can switch over each case using a ``SwitchStore`` and -///// a ``CaseLet`` view per case: -///// -///// ```swift -///// struct AppView: View { -///// let store: StoreOf -///// -///// var body: some View { -///// SwitchStore(self.store) { state in -///// switch state { -///// case .loggedIn: -///// CaseLet( -///// /AppFeature.State.loggedIn, action: AppFeature.Action.loggedIn -///// ) { loggedInStore in -///// LoggedInView(store: loggedInStore) -///// } -///// case .loggedOut: -///// CaseLet( -///// /AppFeature.State.loggedOut, action: AppFeature.Action.loggedOut -///// ) { loggedOutStore in -///// LoggedOutView(store: loggedOutStore) -///// } -///// } -///// } -///// } -///// } -///// ``` -///// -///// > Important: The `SwitchStore` view builder is only evaluated when the case of state passed to -///// > it changes. As such, you should not rely on this value for anything other than checking the -///// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`. -///// -///// See ``Reducer/ifCaseLet(_:action:then:fileID:filePath:line:column:)-7sg8d`` and -///// ``Scope/init(state:action:child:fileID:filePath:line:column:)-9g44g`` for embedding reducers -///// that operate on each case of an enum in reducers that operate on the entire enum. -//@available( -// iOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// macOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// tvOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// watchOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//public struct SwitchStore: View { -// public let store: Store -// public let content: (State) -> Content -// -// public init( -// _ store: Store, -// @ViewBuilder content: @escaping (_ initialState: State) -> Content -// ) { -// self.store = store -// self.content = content -// } -// -// public var body: some View { -// WithViewStore( -// self.store, observe: { $0 }, removeDuplicates: { enumTag($0) == enumTag($1) } -// ) { viewStore in -// self.content(viewStore.state) -// .environmentObject(StoreObservableObject(store: self.store)) -// } -// } -//} -// -///// A view that handles a specific case of enum state in a ``SwitchStore``. -//@available( -// iOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// macOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// tvOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//@available( -// watchOS, deprecated: 9999, -// message: -// "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" -//) -//public struct CaseLet: View { -// public let toCaseState: (EnumState) -> CaseState? -// public let fromCaseAction: (CaseAction) -> EnumAction -// public let content: (Store) -> Content -// -// private let fileID: StaticString -// private let filePath: StaticString -// private let line: UInt -// private let column: UInt -// -// @EnvironmentObject private var store: StoreObservableObject -// -// /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state -// /// matches a particular case. -// /// -// /// - Parameters: -// /// - toCaseState: A function that can extract a case of switch store state, which can be -// /// specified using case path literal syntax, _e.g._ `/State.case`. -// /// - fromCaseAction: A function that can embed a case action in a switch store action. -// /// - content: A function that is given a store of the given case's state and returns a view -// /// that is visible only when the switch store's state matches. -// public init( -// _ toCaseState: @escaping (EnumState) -> CaseState?, -// action fromCaseAction: @escaping (CaseAction) -> EnumAction, -// @ViewBuilder then content: @escaping (_ store: Store) -> Content, -// fileID: StaticString = #fileID, -// filePath: StaticString = #filePath, -// line: UInt = #line, -// column: UInt = #column -// ) { -// self.toCaseState = toCaseState -// self.fromCaseAction = fromCaseAction -// self.content = content -// self.fileID = fileID -// self.filePath = filePath -// self.line = line -// self.column = column -// } -// -// public var body: some View { -// IfLetStore( -// self.store.wrappedValue._scope(state: self.toCaseState, action: self.fromCaseAction), -// then: self.content, -// else: { -// _CaseLetMismatchView( -// fileID: self.fileID, -// filePath: self.filePath, -// line: self.line, -// column: self.column -// ) -// } -// ) -// } -//} -// -//extension CaseLet where EnumAction == CaseAction { -// /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state -// /// matches a particular case. -// /// -// /// - Parameters: -// /// - toCaseState: A function that can extract a case of switch store state, which can be -// /// specified using case path literal syntax, _e.g._ `/State.case`. -// /// - content: A function that is given a store of the given case's state and returns a view -// /// that is visible only when the switch store's state matches. -// public init( -// state toCaseState: @escaping (EnumState) -> CaseState?, -// @ViewBuilder then content: @escaping (_ store: Store) -> Content -// ) { -// self.init( -// toCaseState, -// action: { $0 }, -// then: content -// ) -// } -//} -// -//public struct _CaseLetMismatchView: View { -// @EnvironmentObject private var store: StoreObservableObject -// let fileID: StaticString -// let filePath: StaticString -// let line: UInt -// let column: UInt -// -// public var body: some View { -// #if DEBUG -// let message = """ -// Warning: A "CaseLet" at "\(self.fileID):\(self.line)" was encountered when state was set \ -// to another case: -// -// \(debugCaseOutput(self.store.wrappedValue.withState { $0 })) -// -// This usually happens when there is a mismatch between the case being switched on and the \ -// "CaseLet" view being rendered. -// -// For example, if ".screenA" is being switched on, but the "CaseLet" view is pointed to \ -// ".screenB": -// -// case .screenA: -// CaseLet( -// /State.screenB, action: Action.screenB -// ) { /* ... */ } -// -// Look out for typos to ensure that these two cases align. -// """ -// return VStack(spacing: 17) { -// #if os(macOS) -// Text("⚠️") -// #else -// Image(systemName: "exclamationmark.triangle.fill") -// .font(.largeTitle) -// #endif -// -// Text(message) -// } -// .frame(maxWidth: .infinity, maxHeight: .infinity) -// .foregroundColor(.white) -// .padding() -// .background(Color.red.edgesIgnoringSafeArea(.all)) -// .onAppear { -// reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column) -// } -// #else -// return EmptyView() -// #endif -// } -//} -// -//private final class StoreObservableObject: ObservableObject { -// let wrappedValue: Store -// -// init(store: Store) { -// self.wrappedValue = store -// } -//} -// -//private func enumTag(_ `case`: Case) -> UInt32? { -// EnumMetadata(Case.self)?.tag(of: `case`) -//} +@_spi(Reflection) import CasePaths +import SwiftUI + +/// A view that observes when enum state held in a store changes cases, and provides stores to +/// ``CaseLet`` views. +/// +/// An application may model parts of its state with enums. For example, app state may differ if a +/// user is logged-in or not: +/// +/// ```swift +/// @Reducer +/// struct AppFeature { +/// enum State { +/// case loggedIn(LoggedInState) +/// case loggedOut(LoggedOutState) +/// } +/// // ... +/// } +/// ``` +/// +/// In the view layer, a store on this state can switch over each case using a ``SwitchStore`` and +/// a ``CaseLet`` view per case: +/// +/// ```swift +/// struct AppView: View { +/// let store: StoreOf +/// +/// var body: some View { +/// SwitchStore(self.store) { state in +/// switch state { +/// case .loggedIn: +/// CaseLet( +/// /AppFeature.State.loggedIn, action: AppFeature.Action.loggedIn +/// ) { loggedInStore in +/// LoggedInView(store: loggedInStore) +/// } +/// case .loggedOut: +/// CaseLet( +/// /AppFeature.State.loggedOut, action: AppFeature.Action.loggedOut +/// ) { loggedOutStore in +/// LoggedOutView(store: loggedOutStore) +/// } +/// } +/// } +/// } +/// } +/// ``` +/// +/// > Important: The `SwitchStore` view builder is only evaluated when the case of state passed to +/// > it changes. As such, you should not rely on this value for anything other than checking the +/// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`. +/// +/// See ``Reducer/ifCaseLet(_:action:then:fileID:filePath:line:column:)-7sg8d`` and +/// ``Scope/init(state:action:child:fileID:filePath:line:column:)-9g44g`` for embedding reducers +/// that operate on each case of an enum in reducers that operate on the entire enum. +@available( + iOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + macOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + tvOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + watchOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +public struct SwitchStore: View { + public let store: Store + public let content: (State) -> Content + + public init( + _ store: Store, + @ViewBuilder content: @escaping (_ initialState: State) -> Content + ) { + self.store = store + self.content = content + } + + public var body: some View { + WithViewStore( + self.store, observe: { $0 }, removeDuplicates: { enumTag($0) == enumTag($1) } + ) { viewStore in + self.content(viewStore.state) + .environmentObject(StoreObservableObject(store: self.store)) + } + } +} + +/// A view that handles a specific case of enum state in a ``SwitchStore``. +@available( + iOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + macOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + tvOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +@available( + watchOS, deprecated: 9999, + message: + "Use 'switch' with a store of observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case]" +) +public struct CaseLet: View { + public let toCaseState: (EnumState) -> CaseState? + public let fromCaseAction: (CaseAction) -> EnumAction + public let content: (Store) -> Content + + private let fileID: StaticString + private let filePath: StaticString + private let line: UInt + private let column: UInt + + @EnvironmentObject private var store: StoreObservableObject + + /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state + /// matches a particular case. + /// + /// - Parameters: + /// - toCaseState: A function that can extract a case of switch store state, which can be + /// specified using case path literal syntax, _e.g._ `/State.case`. + /// - fromCaseAction: A function that can embed a case action in a switch store action. + /// - content: A function that is given a store of the given case's state and returns a view + /// that is visible only when the switch store's state matches. + public init( + _ toCaseState: @escaping (EnumState) -> CaseState?, + action fromCaseAction: @escaping (CaseAction) -> EnumAction, + @ViewBuilder then content: @escaping (_ store: Store) -> Content, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) { + self.toCaseState = toCaseState + self.fromCaseAction = fromCaseAction + self.content = content + self.fileID = fileID + self.filePath = filePath + self.line = line + self.column = column + } + + public var body: some View { + IfLetStore( + self.store.wrappedValue._scope(state: self.toCaseState, action: self.fromCaseAction), + then: self.content, + else: { + _CaseLetMismatchView( + fileID: self.fileID, + filePath: self.filePath, + line: self.line, + column: self.column + ) + } + ) + } +} + +extension CaseLet where EnumAction == CaseAction { + /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state + /// matches a particular case. + /// + /// - Parameters: + /// - toCaseState: A function that can extract a case of switch store state, which can be + /// specified using case path literal syntax, _e.g._ `/State.case`. + /// - content: A function that is given a store of the given case's state and returns a view + /// that is visible only when the switch store's state matches. + public init( + state toCaseState: @escaping (EnumState) -> CaseState?, + @ViewBuilder then content: @escaping (_ store: Store) -> Content + ) { + self.init( + toCaseState, + action: { $0 }, + then: content + ) + } +} + +public struct _CaseLetMismatchView: View { + @EnvironmentObject private var store: StoreObservableObject + let fileID: StaticString + let filePath: StaticString + let line: UInt + let column: UInt + + public var body: some View { + #if DEBUG + let message = """ + Warning: A "CaseLet" at "\(self.fileID):\(self.line)" was encountered when state was set \ + to another case: + + \(debugCaseOutput(self.store.wrappedValue.withState { $0 })) + + This usually happens when there is a mismatch between the case being switched on and the \ + "CaseLet" view being rendered. + + For example, if ".screenA" is being switched on, but the "CaseLet" view is pointed to \ + ".screenB": + + case .screenA: + CaseLet( + /State.screenB, action: Action.screenB + ) { /* ... */ } + + Look out for typos to ensure that these two cases align. + """ + return VStack(spacing: 17) { + #if os(macOS) + Text("⚠️") + #else + Image(systemName: "exclamationmark.triangle.fill") + .font(.largeTitle) + #endif + + Text(message) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.white) + .padding() + .background(Color.red.edgesIgnoringSafeArea(.all)) + .onAppear { + reportIssue(message, fileID: fileID, filePath: filePath, line: line, column: column) + } + #else + return EmptyView() + #endif + } +} + +private final class StoreObservableObject: ObservableObject { + let wrappedValue: Store + + init(store: Store) { + self.wrappedValue = store + } +} + +private func enumTag(_ `case`: Case) -> UInt32? { + EnumMetadata(Case.self)?.tag(of: `case`) +} From 066558563a943dd246121d562e87aaf5b1b133c9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:43:59 -0700 Subject: [PATCH 10/26] wip --- .../SwiftUI/ForEachStore.swift | 500 +++++++++--------- 1 file changed, 258 insertions(+), 242 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift index 641b6c509444..5f6ec10ea78f 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift @@ -1,242 +1,258 @@ -//import OrderedCollections -//import SwiftUI -// -///// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with -///// collections of state. -///// -///// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each -///// element. This allows you to extract and modularize an element's view and avoid concerns around -///// collection index math and parent-child store communication. -///// -///// For example, a todos app may define the domain and logic associated with an individual todo: -///// -///// ```swift -///// @Reducer -///// struct Todo { -///// struct State: Equatable, Identifiable { -///// let id: UUID -///// var description = "" -///// var isComplete = false -///// } -///// -///// enum Action { -///// case isCompleteToggled(Bool) -///// case descriptionChanged(String) -///// } -///// -///// var body: some Reducer { -///// // ... -///// } -///// } -///// ``` -///// -///// As well as a view with a domain-specific store: -///// -///// ```swift -///// struct TodoView: View { -///// let store: StoreOf -///// var body: some View { /* ... */ } -///// } -///// ``` -///// -///// For a parent domain to work with a collection of todos, it can hold onto this collection in -///// state: -///// -///// ```swift -///// @Reducer -///// struct Todos { -///// struct State: Equatable { -///// var todos: IdentifiedArrayOf = [] -///// } -///// // ... -///// } -///// ``` -///// -///// Define a case to handle actions sent to the child domain: -///// -///// ```swift -///// enum Action { -///// case todos(IdentifiedActionOf) -///// } -///// ``` -///// -///// Enhance its core reducer using -///// ``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-3dw7i``: -///// -///// ```swift -///// var body: some Reducer { -///// Reduce { state, action in -///// // ... -///// } -///// .forEach(\.todos, action: \.todos) { -///// Todo() -///// } -///// } -///// ``` -///// -///// And finally render a list of `TodoView`s using ``ForEachStore``: -///// -///// ```swift -///// ForEachStore( -///// self.store.scope(state: \.todos, action: \.todos) -///// ) { todoStore in -///// TodoView(store: todoStore) -///// } -///// ``` -///// -//@available( -// iOS, deprecated: 9999, -// message: -// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -//) -//@available( -// macOS, deprecated: 9999, -// message: -// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -//) -//@available( -// tvOS, deprecated: 9999, -// message: -// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -//) -//@available( -// watchOS, deprecated: 9999, -// message: -// "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" -//) -//public struct ForEachStore< -// EachState, EachAction, Data: Collection, ID: Hashable & Sendable, Content: View -//>: View { -// public let data: Data -// let content: Content -// -// /// Initializes a structure that computes views on demand from a store on a collection of data and -// /// an identified action. -// /// -// /// - Parameters: -// /// - store: A store on an identified array of data and an identified action. -// /// - content: A function that can generate content given a store of an element. -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, IdentifiedAction>, -// @ViewBuilder content: @escaping (_ store: Store) -> EachContent -// ) -// where -// Data == IdentifiedArray, -// Content == WithViewStore< -// IdentifiedArray, IdentifiedAction, -// ForEach, ID, EachContent> -// > -// { -// self.data = store.withState { $0 } -// self.content = WithViewStore( -// store, -// observe: { $0 }, -// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } -// ) { viewStore in -// ForEach(viewStore.state, id: viewStore.state.id) { element in -// let id = element[keyPath: viewStore.state.id] -// var element = element -// content( -// store.scope( -// id: store.id(state: \.[id:id]!, action: \.[id:id]), -// state: ToState { -// element = $0[id: id] ?? element -// return element -// }, -// action: { .element(id: id, action: $0) }, -// isInvalid: { !$0.ids.contains(id) } -// ) -// ) -// } -// } -// } -// -// @available( -// iOS, -// deprecated: 9999, -// message: -// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" -// ) -// @available( -// macOS, -// deprecated: 9999, -// message: -// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" -// ) -// @available( -// tvOS, -// deprecated: 9999, -// message: -// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" -// ) -// @available( -// watchOS, -// deprecated: 9999, -// message: -// "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public init( -// _ store: Store, (id: ID, action: EachAction)>, -// @ViewBuilder content: @escaping (_ store: Store) -> EachContent -// ) -// where -// Data == IdentifiedArray, -// Content == WithViewStore< -// IdentifiedArray, (id: ID, action: EachAction), -// ForEach, ID, EachContent> -// > -// { -// self.data = store.withState { $0 } -// self.content = WithViewStore( -// store, -// observe: { $0 }, -// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } -// ) { viewStore in -// ForEach(viewStore.state, id: viewStore.state.id) { element in -// var element = element -// let id = element[keyPath: viewStore.state.id] -// content( -// store.scope( -// id: store.id(state: \.[id:id]!, action: \.[id:id]), -// state: ToState { -// element = $0[id: id] ?? element -// return element -// }, -// action: { (id, $0) }, -// isInvalid: { !$0.ids.contains(id) } -// ) -// ) -// } -// } -// } -// -// public var body: some View { -// self.content -// } -//} -// -//#if compiler(>=6) -// extension ForEachStore: @preconcurrency DynamicViewContent {} -//#else -// extension ForEachStore: DynamicViewContent {} -//#endif -// -//extension Case { -// fileprivate subscript(id id: ID) -> Case -// where Value == (id: ID, action: Action) { -// Case( -// embed: { (id: id, action: $0) }, -// extract: { $0.id == id ? $0.action : nil } -// ) -// } -//} +import OrderedCollections +import SwiftUI + +/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with +/// collections of state. +/// +/// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each +/// element. This allows you to extract and modularize an element's view and avoid concerns around +/// collection index math and parent-child store communication. +/// +/// For example, a todos app may define the domain and logic associated with an individual todo: +/// +/// ```swift +/// @Reducer +/// struct Todo { +/// struct State: Equatable, Identifiable { +/// let id: UUID +/// var description = "" +/// var isComplete = false +/// } +/// +/// enum Action { +/// case isCompleteToggled(Bool) +/// case descriptionChanged(String) +/// } +/// +/// var body: some Reducer { +/// // ... +/// } +/// } +/// ``` +/// +/// As well as a view with a domain-specific store: +/// +/// ```swift +/// struct TodoView: View { +/// let store: StoreOf +/// var body: some View { /* ... */ } +/// } +/// ``` +/// +/// For a parent domain to work with a collection of todos, it can hold onto this collection in +/// state: +/// +/// ```swift +/// @Reducer +/// struct Todos { +/// struct State: Equatable { +/// var todos: IdentifiedArrayOf = [] +/// } +/// // ... +/// } +/// ``` +/// +/// Define a case to handle actions sent to the child domain: +/// +/// ```swift +/// enum Action { +/// case todos(IdentifiedActionOf) +/// } +/// ``` +/// +/// Enhance its core reducer using +/// ``Reducer/forEach(_:action:element:fileID:filePath:line:column:)-3dw7i``: +/// +/// ```swift +/// var body: some Reducer { +/// Reduce { state, action in +/// // ... +/// } +/// .forEach(\.todos, action: \.todos) { +/// Todo() +/// } +/// } +/// ``` +/// +/// And finally render a list of `TodoView`s using ``ForEachStore``: +/// +/// ```swift +/// ForEachStore( +/// self.store.scope(state: \.todos, action: \.todos) +/// ) { todoStore in +/// TodoView(store: todoStore) +/// } +/// ``` +/// +@available( + iOS, deprecated: 9999, + message: + "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +) +@available( + macOS, deprecated: 9999, + message: + "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +) +@available( + tvOS, deprecated: 9999, + message: + "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +) +@available( + watchOS, deprecated: 9999, + message: + "Pass 'ForEach' a store scoped to an identified array and identified action, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach]" +) +public struct ForEachStore< + EachState, EachAction, Data: Collection, ID: Hashable & Sendable, Content: View +>: View { + public let data: Data + let content: Content + + /// Initializes a structure that computes views on demand from a store on a collection of data and + /// an identified action. + /// + /// - Parameters: + /// - store: A store on an identified array of data and an identified action. + /// - content: A function that can generate content given a store of an element. + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, IdentifiedAction>, + @ViewBuilder content: @escaping (_ store: Store) -> EachContent + ) + where + Data == IdentifiedArray, + Content == WithViewStore< + IdentifiedArray, IdentifiedAction, + ForEach, ID, EachContent> + > + { + self.data = store.withState { $0 } + + func open( + _ core: some Core, IdentifiedAction>, + element: EachState, + id: ID + ) -> any Core { + IfLetCore( + base: core, + cachedState: element, + stateKeyPath: \.[id:id], + actionKeyPath: \.[id:id] + ) + } + + self.content = WithViewStore( + store, + observe: { $0 }, + removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } + ) { viewStore in + ForEach(viewStore.state, id: viewStore.state.id) { element in + let id = element[keyPath: viewStore.state.id] + content( + store.scope( + id: store.id(state: \.[id:id]!, action: \.[id:id]), + childCore: open(store.core, element: element, id: id) + ) + ) + } + } + } + + @available( + iOS, + deprecated: 9999, + message: + "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" + ) + @available( + macOS, + deprecated: 9999, + message: + "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" + ) + @available( + tvOS, + deprecated: 9999, + message: + "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" + ) + @available( + watchOS, + deprecated: 9999, + message: + "Use an 'IdentifiedAction', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Identified-actions" + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public init( + _ store: Store, (id: ID, action: EachAction)>, + @ViewBuilder content: @escaping (_ store: Store) -> EachContent + ) + where + Data == IdentifiedArray, + Content == WithViewStore< + IdentifiedArray, (id: ID, action: EachAction), + ForEach, ID, EachContent> + > + { + self.data = store.withState { $0 } + + func open( + _ core: some Core, (id: ID, action: EachAction)>, + element: EachState, + id: ID + ) -> any Core { + IfLetCore( + base: core, + cachedState: element, + stateKeyPath: \.[id:id], + actionKeyPath: \.[id:id] + ) + } + + self.content = WithViewStore( + store, + observe: { $0 }, + removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } + ) { viewStore in + ForEach(viewStore.state, id: viewStore.state.id) { element in + let id = element[keyPath: viewStore.state.id] + content( + store.scope( + id: store.id(state: \.[id:id]!, action: \.[id:id]), + childCore: open(store.core, element: element, id: id) + ) + ) + } + } + } + + public var body: some View { + self.content + } +} + +#if compiler(>=6) + extension ForEachStore: @preconcurrency DynamicViewContent {} +#else + extension ForEachStore: DynamicViewContent {} +#endif + +extension Case { + fileprivate subscript(id id: ID) -> Case + where Value == (id: ID, action: Action) { + Case( + embed: { (id: id, action: $0) }, + extract: { $0.id == id ? $0.action : nil } + ) + } +} From a5e773461ce498b38c9ddc0fea77771e1e58e9e8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:44:29 -0700 Subject: [PATCH 11/26] wip --- .../SwiftUI/FullScreenCover.swift | 214 +++++++++--------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift b/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift index 19f4d876e11d..b4f5acb11c96 100644 --- a/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift +++ b/Sources/ComposableArchitecture/SwiftUI/FullScreenCover.swift @@ -1,107 +1,107 @@ -//import SwiftUI -// -//#if !os(macOS) -// @available(iOS 14, tvOS 14, watchOS 7, *) -// @available(macOS, unavailable) -// extension View { -// /// Presents a modal view that covers as much of the screen as possible using the store you -// /// provide as a data source for the sheet's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view -// /// > modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet -// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the -// /// system dismisses the currently displayed sheet. -// /// - onDismiss: The closure to execute when dismissing the modal view. -// /// - content: A closure returning the content of the modal view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" -// ) -// public func fullScreenCover( -// store: Store, PresentationAction>, -// onDismiss: (() -> Void)? = nil, -// @ViewBuilder content: @escaping (_ store: Store) -> Content -// ) -> some View { -// self.presentation(store: store) { `self`, $item, destination in -// self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in -// destination(content) -// } -// } -// } -// -// /// Presents a modal view that covers as much of the screen as possible using the store you -// /// provide as a data source for the sheet's content. -// /// -// /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view -// /// > modifier. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for -// /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` -// /// and `Action` to the modifier's closure. You use this store to power the content in a sheet -// /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the -// /// system dismisses the currently displayed sheet. -// /// - toDestinationState: A transformation to extract modal state from the presentation state. -// /// - fromDestinationAction: A transformation to embed modal actions into the presentation -// /// action. -// /// - onDismiss: The closure to execute when dismissing the modal view. -// /// - content: A closure returning the content of the modal view. -// @available( -// iOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// macOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// tvOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// @available( -// watchOS, deprecated: 9999, -// message: -// "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" -// ) -// public func fullScreenCover( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// onDismiss: (() -> Void)? = nil, -// @ViewBuilder content: @escaping (_ store: Store) -> -// Content -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, destination in -// self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in -// destination(content) -// } -// } -// } -// } -//#endif +import SwiftUI + +#if !os(macOS) + @available(iOS 14, tvOS 14, watchOS 7, *) + @available(macOS, unavailable) + extension View { + /// Presents a modal view that covers as much of the screen as possible using the store you + /// provide as a data source for the sheet's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view + /// > modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a sheet + /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the + /// system dismisses the currently displayed sheet. + /// - onDismiss: The closure to execute when dismissing the modal view. + /// - content: A closure returning the content of the modal view. + @available( + iOS, deprecated: 9999, + message: + "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + macOS, deprecated: 9999, + message: + "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + tvOS, deprecated: 9999, + message: + "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + @available( + watchOS, deprecated: 9999, + message: + "Pass a binding of a store to 'fullScreenCover(item:)' instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-navigation-view-modifiers-with-SwiftUI-modifiers]" + ) + public func fullScreenCover( + store: Store, PresentationAction>, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (_ store: Store) -> Content + ) -> some View { + self.presentation(store: store) { `self`, $item, destination in + self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in + destination(content) + } + } + } + + /// Presents a modal view that covers as much of the screen as possible using the store you + /// provide as a data source for the sheet's content. + /// + /// > This is a Composable Architecture-friendly version of SwiftUI's `fullScreenCover` view + /// > modifier. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for + /// a modal. When `store`'s state is non-`nil`, the system passes a store of unwrapped `State` + /// and `Action` to the modifier's closure. You use this store to power the content in a sheet + /// you create that the system displays to the user. If `store`'s state is `nil`-ed out, the + /// system dismisses the currently displayed sheet. + /// - toDestinationState: A transformation to extract modal state from the presentation state. + /// - fromDestinationAction: A transformation to embed modal actions into the presentation + /// action. + /// - onDismiss: The closure to execute when dismissing the modal view. + /// - content: A closure returning the content of the modal view. + @available( + iOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + macOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + tvOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + @available( + watchOS, deprecated: 9999, + message: + "Further scope the store into the 'state' and 'action' cases, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs" + ) + public func fullScreenCover( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (_ store: Store) -> + Content + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, destination in + self.fullScreenCover(item: $item, onDismiss: onDismiss) { _ in + destination(content) + } + } + } + } +#endif From 0d630e2e8f909f73d99d2e9f58127ce7efb2dffd Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:48:29 -0700 Subject: [PATCH 12/26] wip --- .../SwiftUI/NavigationStackStore.swift | 406 +++++++++--------- 1 file changed, 197 insertions(+), 209 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift b/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift index 20b1f2e7241b..6022a54b3df2 100644 --- a/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift @@ -1,209 +1,197 @@ -//import OrderedCollections -//import SwiftUI -// -///// A navigation stack that is driven by a store. -///// -///// This view can be used to drive stack-based navigation in the Composable Architecture when passed -///// a store that is focused on ``StackState`` and ``StackAction``. -///// -///// See the dedicated article on for more information on the library's navigation -///// tools, and in particular see for information on using this view. -//@available( -// iOS, deprecated: 9999, -// message: -// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -//) -//@available( -// macOS, deprecated: 9999, -// message: -// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -//) -//@available( -// tvOS, deprecated: 9999, -// message: -// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -//) -//@available( -// watchOS, deprecated: 9999, -// message: -// "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" -//) -//@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -//public struct NavigationStackStore: View { -// private let root: Root -// private let destination: (StackState.Component) -> Destination -// @ObservedObject private var viewStore: ViewStore, StackAction> -// -// /// Creates a navigation stack with a store of stack state and actions. -// /// -// /// - Parameters: -// /// - path: A store of stack state and actions to power this stack. -// /// - root: The view to display when the stack is empty. -// /// - destination: A view builder that defines a view to display when an element is appended to -// /// the stack's state. The closure takes one argument, which is a store of the value to -// /// present. -// public init( -// _ store: Store, StackAction>, -// @ViewBuilder root: () -> Root, -// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, -// fileID: StaticString = #fileID, -// filePath: StaticString = #filePath, -// line: UInt = #line, -// column: UInt = #column -// ) { -// func navigationDestination( -// component: StackState.Component -// ) -> Destination { -// let id = store.id( -// state: -// \.[ -// id:component.id, -// fileID:_HashableStaticString(rawValue: fileID), -// filePath:_HashableStaticString(rawValue: filePath), -// line:line, -// column:column -// ], -// action: \.[id:component.id] -// ) -// if let child = store.children[id] as? Store { -// return destination(child) -// } else { -// @MainActor -// func open( -// _ core: some Core, StackAction> -// ) -> Destination { -// let child = Store( -// core: IfLetCore( -// base: core, -// cachedState: component.element, -// stateKeyPath: \.[ -// id:component.id, -// fileID:_HashableStaticString(rawValue: fileID), -// filePath:_HashableStaticString(rawValue: filePath), -// line:line, -// column:column -// ], -// actionKeyPath: \.[id:component.id] -// ) -// ) -// store.children[id] = child -// return destination(child) -// } -// return open(store.core) -// } -// } -// self.root = root() -// self.destination = navigationDestination(component:) -// self._viewStore = ObservedObject( -// wrappedValue: ViewStore( -// store, -// observe: { $0 }, -// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } -// ) -// ) -// } -// -// /// Creates a navigation stack with a store of stack state and actions. -// /// -// /// - Parameters: -// /// - path: A store of stack state and actions to power this stack. -// /// - root: The view to display when the stack is empty. -// /// - destination: A view builder that defines a view to display when an element is appended to -// /// the stack's state. The closure takes one argument, which is the initial enum state to -// /// present. You can switch over this value and use ``CaseLet`` views to handle each case. -// @_disfavoredOverload -// public init( -// _ store: Store, StackAction>, -// @ViewBuilder root: () -> Root, -// @ViewBuilder destination: @escaping (_ initialState: State) -> D, -// fileID: StaticString = #fileID, -// filePath: StaticString = #filePath, -// line: UInt = #line, -// column: UInt = #column -// ) where Destination == SwitchStore { -// func navigationDestination( -// component: StackState.Component -// ) -> Destination { -// let id = store.id( -// state: -// \.[ -// id:component.id, -// fileID:_HashableStaticString(rawValue: fileID), -// filePath:_HashableStaticString(rawValue: filePath), -// line:line, -// column:column -// ], -// action: \.[id:component.id] -// ) -// if let child = store.children[id] as? Store { -// return SwitchStore(child, content: destination) -// } else { -// @MainActor -// func open( -// _ core: some Core, StackAction> -// ) -> Destination { -// let child = Store( -// core: IfLetCore( -// base: core, -// cachedState: component.element, -// stateKeyPath: \.[ -// id:component.id, -// fileID:_HashableStaticString(rawValue: fileID), -// filePath:_HashableStaticString(rawValue: filePath), -// line:line, -// column:column -// ], -// actionKeyPath: \.[id:component.id] -// ) -// ) -// store.children[id] = child -// return SwitchStore(child, content: destination) -// } -// return open(store.core) -// } -// } -// -// self.root = root() -// self.destination = navigationDestination(component:) -// self._viewStore = ObservedObject( -// wrappedValue: ViewStore( -// store, -// observe: { $0 }, -// removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } -// ) -// ) -// } -// -// public var body: some View { -// NavigationStack( -// path: self.viewStore.binding( -// get: { $0.path }, -// compactSend: { newPath in -// if newPath.count > self.viewStore.path.count, let component = newPath.last { -// return .push(id: component.id, state: component.element) -// } else if newPath.count < self.viewStore.path.count { -// return .popFrom(id: self.viewStore.path[newPath.count].id) -// } else { -// return nil -// } -// } -// ) -// ) { -// self.root -// .environment(\.navigationDestinationType, State.self) -// .navigationDestination(for: StackState.Component.self) { component in -// NavigationDestinationView(component: component, destination: self.destination) -// } -// } -// } -//} -// -//private struct NavigationDestinationView: View { -// let component: StackState.Component -// let destination: (StackState.Component) -> Destination -// var body: some View { -// self.destination(self.component) -// .environment(\.navigationDestinationType, State.self) -// .id(self.component.id) -// } -//} +import OrderedCollections +import SwiftUI + +/// A navigation stack that is driven by a store. +/// +/// This view can be used to drive stack-based navigation in the Composable Architecture when passed +/// a store that is focused on ``StackState`` and ``StackAction``. +/// +/// See the dedicated article on for more information on the library's navigation +/// tools, and in particular see for information on using this view. +@available( + iOS, deprecated: 9999, + message: + "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +) +@available( + macOS, deprecated: 9999, + message: + "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +) +@available( + tvOS, deprecated: 9999, + message: + "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +) +@available( + watchOS, deprecated: 9999, + message: + "Use 'NavigationStack.init(path:)' with a store scoped from observable state, instead. For more information, see the following article: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack]" +) +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +public struct NavigationStackStore: View { + private let root: Root + private let destination: (StackState.Component) -> Destination + @ObservedObject private var viewStore: ViewStore, StackAction> + + /// Creates a navigation stack with a store of stack state and actions. + /// + /// - Parameters: + /// - path: A store of stack state and actions to power this stack. + /// - root: The view to display when the stack is empty. + /// - destination: A view builder that defines a view to display when an element is appended to + /// the stack's state. The closure takes one argument, which is a store of the value to + /// present. + public init( + _ store: Store, StackAction>, + @ViewBuilder root: () -> Root, + @ViewBuilder destination: @escaping (_ store: Store) -> Destination, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) { + func navigationDestination( + component: StackState.Component + ) -> Destination { + let id = store.id( + state: + \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + action: \.[id:component.id] + ) + @MainActor + func open( + _ core: some Core, StackAction> + ) -> any Core { + IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) + } + return destination(store.scope(id: id, childCore: open(store.core))) + } + self.root = root() + self.destination = navigationDestination(component:) + self._viewStore = ObservedObject( + wrappedValue: ViewStore( + store, + observe: { $0 }, + removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } + ) + ) + } + + /// Creates a navigation stack with a store of stack state and actions. + /// + /// - Parameters: + /// - path: A store of stack state and actions to power this stack. + /// - root: The view to display when the stack is empty. + /// - destination: A view builder that defines a view to display when an element is appended to + /// the stack's state. The closure takes one argument, which is the initial enum state to + /// present. You can switch over this value and use ``CaseLet`` views to handle each case. + @_disfavoredOverload + public init( + _ store: Store, StackAction>, + @ViewBuilder root: () -> Root, + @ViewBuilder destination: @escaping (_ initialState: State) -> D, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) where Destination == SwitchStore { + func navigationDestination( + component: StackState.Component + ) -> Destination { + let id = store.id( + state: + \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + action: \.[id:component.id] + ) + if let child = store.children[id] as? Store { + return SwitchStore(child, content: destination) + } else { + @MainActor + func open( + _ core: some Core, StackAction> + ) -> any Core { + IfLetCore( + base: core, + cachedState: component.element, + stateKeyPath: \.[ + id:component.id, + fileID:_HashableStaticString(rawValue: fileID), + filePath:_HashableStaticString(rawValue: filePath), + line:line, + column:column + ], + actionKeyPath: \.[id:component.id] + ) + } + return SwitchStore(store.scope(id: id, childCore: open(store.core)), content: destination) + } + } + + self.root = root() + self.destination = navigationDestination(component:) + self._viewStore = ObservedObject( + wrappedValue: ViewStore( + store, + observe: { $0 }, + removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) } + ) + ) + } + + public var body: some View { + NavigationStack( + path: self.viewStore.binding( + get: { $0.path }, + compactSend: { newPath in + if newPath.count > self.viewStore.path.count, let component = newPath.last { + return .push(id: component.id, state: component.element) + } else if newPath.count < self.viewStore.path.count { + return .popFrom(id: self.viewStore.path[newPath.count].id) + } else { + return nil + } + } + ) + ) { + self.root + .environment(\.navigationDestinationType, State.self) + .navigationDestination(for: StackState.Component.self) { component in + NavigationDestinationView(component: component, destination: self.destination) + } + } + } +} + +private struct NavigationDestinationView: View { + let component: StackState.Component + let destination: (StackState.Component) -> Destination + var body: some View { + self.destination(self.component) + .environment(\.navigationDestinationType, State.self) + .id(self.component.id) + } +} From edbf4e64315a0d09242774fd1fbbe0ed4f0a9754 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 14:55:44 -0700 Subject: [PATCH 13/26] wip --- Sources/ComposableArchitecture/Core.swift | 14 +++++++++- Sources/ComposableArchitecture/Store.swift | 1 + .../SwiftUI/IfLetStore.swift | 2 ++ .../SwiftUI/PresentationModifier.swift | 2 ++ .../StoreFilterTests.swift | 28 ------------------- .../StoreTests.swift | 20 ++++++------- 6 files changed, 28 insertions(+), 39 deletions(-) delete mode 100644 Tests/ComposableArchitectureTests/StoreFilterTests.swift diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index c296b7686118..baa0173aae57 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -11,6 +11,8 @@ protocol Core: AnyObject, Sendable { var canStoreCacheChildren: Bool { get } var didSet: CurrentValueRelay { get } var isInvalid: Bool { get } + + var effectCancellables: [UUID: AnyCancellable] { get } } final class InvalidCore: Core { @@ -23,6 +25,7 @@ final class InvalidCore: Core { var canStoreCacheChildren: Bool { false } let didSet = CurrentValueRelay(()) var isInvalid: Bool { true } + var effectCancellables: [UUID: AnyCancellable] { [:] } } final class RootCore: Core { @@ -38,7 +41,7 @@ final class RootCore: Core { var isInvalid: Bool { false } private var bufferedActions: [Root.Action] = [] - private var effectCancellables: [UUID: AnyCancellable] = [:] + var effectCancellables: [UUID: AnyCancellable] = [:] private var isSending = false init( initialState: Root.State, @@ -206,6 +209,9 @@ class ScopedCore: Core { var isInvalid: Bool { base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { + base.effectCancellables + } } class IfLetCore: Core { @@ -244,6 +250,9 @@ class IfLetCore: Core { var isInvalid: Bool { base.state[keyPath: stateKeyPath] == nil || base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { + base.effectCancellables + } } class ClosureScopedCore: Core { @@ -274,4 +283,7 @@ class ClosureScopedCore: Core { var isInvalid: Bool { base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { + base.effectCancellables + } } diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 5cd59b9eabd5..e212767d3450 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -141,6 +141,7 @@ public final class Store { var children: [ScopeID: AnyObject] = [:] let core: any Core + @_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] { core.effectCancellables } #if !os(visionOS) let _$observationRegistrar = PerceptionRegistrar( diff --git a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift index 35832b5462b7..fef4ec46c0b0 100644 --- a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift @@ -1,3 +1,4 @@ +import Combine import SwiftUI /// A view that safely unwraps a store of optional state in order to show one of two views. @@ -315,4 +316,5 @@ private final class _IfLetCore, Wrapped, Action>: C var canStoreCacheChildren: Bool { base.canStoreCacheChildren } var didSet: CurrentValueRelay { base.didSet } var isInvalid: Bool { state == nil || base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } } diff --git a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift index 6b98a058a7f5..85f19027e1da 100644 --- a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift +++ b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift @@ -1,3 +1,4 @@ +import Combine import SwiftUI extension View { @@ -349,6 +350,7 @@ private final class PresentationCore< var canStoreCacheChildren: Bool { base.canStoreCacheChildren } var didSet: CurrentValueRelay { base.didSet } var isInvalid: Bool { state.wrappedValue.flatMap(toDestinationState) == nil || base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } } @_spi(Presentation) diff --git a/Tests/ComposableArchitectureTests/StoreFilterTests.swift b/Tests/ComposableArchitectureTests/StoreFilterTests.swift deleted file mode 100644 index 66c5a271061e..000000000000 --- a/Tests/ComposableArchitectureTests/StoreFilterTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Combine -@_spi(Internals) import ComposableArchitecture -import XCTest - -final class StoreInvalidationTests: BaseTCATestCase { - func testInvalidation() { - var cancellables: Set = [] - - let store = Store(initialState: nil) {} - .scope( - id: nil, - state: ToState { $0 }, - action: { $0 }, - isInvalid: { $0 != nil } - ) - let viewStore = ViewStore(store, observe: { $0 }) - var count = 0 - viewStore.publisher - .sink { _ in count += 1 } - .store(in: &cancellables) - - XCTAssertEqual(count, 1) - viewStore.send(()) - XCTAssertEqual(count, 1) - viewStore.send(()) - XCTAssertEqual(count, 1) - } -} diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index 2dd3f0182031..2a1db56f72c8 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -9,11 +9,11 @@ final class StoreTests: BaseTCATestCase { func testCancellableIsRemovedOnImmediatelyCompletingEffect() { let store = Store(initialState: ()) {} - XCTAssertEqual(store.rootStore.effectCancellables.count, 0) + XCTAssertEqual(store.effectCancellables.count, 0) store.send(()) - XCTAssertEqual(store.rootStore.effectCancellables.count, 0) + XCTAssertEqual(store.effectCancellables.count, 0) } @MainActor @@ -35,15 +35,15 @@ final class StoreTests: BaseTCATestCase { }) let store = Store(initialState: ()) { reducer } - XCTAssertEqual(store.rootStore.effectCancellables.count, 0) + XCTAssertEqual(store.effectCancellables.count, 0) store.send(.start) - XCTAssertEqual(store.rootStore.effectCancellables.count, 1) + XCTAssertEqual(store.effectCancellables.count, 1) mainQueue.advance(by: 2) - XCTAssertEqual(store.rootStore.effectCancellables.count, 0) + XCTAssertEqual(store.effectCancellables.count, 0) } @available(*, deprecated) @@ -571,12 +571,12 @@ final class StoreTests: BaseTCATestCase { } let scopedStore = store.scope(state: { $0 }, action: { $0 }) - let sendTask = scopedStore.send((), originatingFrom: nil) + let sendTask: Task? = scopedStore.send(()) await Task.yield() neverEndingTask.cancel() try await XCTUnwrap(sendTask).value - XCTAssertEqual(store.rootStore.effectCancellables.count, 0) - XCTAssertEqual(scopedStore.rootStore.effectCancellables.count, 0) + XCTAssertEqual(store.effectCancellables.count, 0) + XCTAssertEqual(scopedStore.effectCancellables.count, 0) } @Reducer @@ -692,7 +692,7 @@ final class StoreTests: BaseTCATestCase { let store = Store(initialState: Feature_testStoreVsTestStore.State()) { Feature_testStoreVsTestStore() } - await store.send(.tap, originatingFrom: nil)?.value + await store.send(.tap)?.value XCTAssertEqual(store.withState(\.count), testStore.state.count) } @@ -754,7 +754,7 @@ final class StoreTests: BaseTCATestCase { let store = Store(initialState: Feature_testStoreVsTestStore_Publisher.State()) { Feature_testStoreVsTestStore_Publisher() } - await store.send(.tap, originatingFrom: nil)?.value + await store.send(.tap)?.value XCTAssertEqual(store.withState(\.count), testStore.state.count) } From 26e41f99882987754c58db8a416aba6434cbe92f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 15:08:07 -0700 Subject: [PATCH 14/26] wip --- Sources/ComposableArchitecture/Store.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index e212767d3450..0c4ffa960d36 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -1,4 +1,4 @@ -@preconcurrency import Combine +import Combine import Foundation import SwiftUI From f4da4c639b12f9a584e509e7e3b7db805510913d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 15:11:20 -0700 Subject: [PATCH 15/26] wip --- .../SwiftUI/Deprecated/ActionSheet.swift | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift index 978fb00bdfbb..bacb3608bed8 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/ActionSheet.swift @@ -1,98 +1,98 @@ -//import SwiftUI -// -//extension View { -// /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it -// /// becomes `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// /// - toDestinationState: A transformation to extract alert state from the presentation state. -// /// - fromDestinationAction: A transformation to embed alert actions into the presentation -// /// action. -// @available( -// iOS, -// introduced: 13, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:)' instead." -// ) -// @available(macOS, unavailable) -// @available( -// tvOS, -// introduced: 13, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:)' instead." -// ) -// @available( -// watchOS, -// introduced: 6, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:)' instead." -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func actionSheet( -// store: Store< -// PresentationState>, PresentationAction -// > -// ) -> some View { -// self.actionSheet(store: store, state: { $0 }, action: { $0 }) -// } -// -// /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes -// /// `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// /// - toDestinationState: A transformation to extract alert state from the presentation state. -// /// - fromDestinationAction: A transformation to embed alert actions into the presentation -// /// action. -// @available( -// iOS, -// introduced: 13, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:state:action:)' instead." -// ) -// @available(macOS, unavailable) -// @available( -// tvOS, -// introduced: 13, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:state:action:)' instead." -// ) -// @available( -// watchOS, -// introduced: 6, -// deprecated: 100000, -// message: "use 'View.confirmationDialog(store:state:action:)' instead." -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func actionSheet( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, -// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, _ in -// let actionSheetState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } -// self.actionSheet(item: $item) { _ in -// ActionSheet(actionSheetState!) { action in -// if let action { -// store.send(.presented(fromDestinationAction(action))) -// } else { -// store.send(.dismiss) -// } -// } -// } -// } -// } -//} +import SwiftUI + +extension View { + /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it + /// becomes `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + /// - toDestinationState: A transformation to extract alert state from the presentation state. + /// - fromDestinationAction: A transformation to embed alert actions into the presentation + /// action. + @available( + iOS, + introduced: 13, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:)' instead." + ) + @available(macOS, unavailable) + @available( + tvOS, + introduced: 13, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:)' instead." + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func actionSheet( + store: Store< + PresentationState>, PresentationAction + > + ) -> some View { + self.actionSheet(store: store, state: { $0 }, action: { $0 }) + } + + /// Displays an alert when the store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + /// - toDestinationState: A transformation to extract alert state from the presentation state. + /// - fromDestinationAction: A transformation to embed alert actions into the presentation + /// action. + @available( + iOS, + introduced: 13, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:state:action:)' instead." + ) + @available(macOS, unavailable) + @available( + tvOS, + introduced: 13, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:state:action:)' instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "use 'View.confirmationDialog(store:state:action:)' instead." + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func actionSheet( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> ConfirmationDialogState?, + action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, _ in + let actionSheetState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } + self.actionSheet(item: $item) { _ in + ActionSheet(actionSheetState!) { action in + if let action { + store.send(.presented(fromDestinationAction(action))) + } else { + store.send(.dismiss) + } + } + } + } + } +} From 33cc53e8b8f7e4775306450bae3fefd524defcec Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 15:11:50 -0700 Subject: [PATCH 16/26] wip --- .../SwiftUI/Deprecated/LegacyAlert.swift | 174 +++++++++--------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift index 76f3bfa0650b..21ee708eb5f8 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/LegacyAlert.swift @@ -1,87 +1,87 @@ -//import SwiftUI -// -//extension View { -// /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it -// /// becomes `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// @available(iOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") -// @available( -// macOS, introduced: 10.15, deprecated: 100000, message: "use `View.alert(store:) instead." -// ) -// @available(tvOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") -// @available( -// watchOS, introduced: 6, deprecated: 100000, message: "use `View.alert(store:) instead." -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func legacyAlert( -// store: Store>, PresentationAction> -// ) -> some View { -// self.legacyAlert(store: store, state: { $0 }, action: { $0 }) -// } -// -// /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it -// /// becomes `nil`. -// /// -// /// - Parameters: -// /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an -// /// alert. -// /// - toDestinationState: A transformation to extract alert state from the presentation state. -// /// - fromDestinationAction: A transformation to embed alert actions into the presentation -// /// action. -// @available( -// iOS, -// introduced: 13, -// deprecated: 100000, -// message: "use `View.alert(store:state:action:) instead." -// ) -// @available( -// macOS, -// introduced: 10.15, -// deprecated: 100000, -// message: "use `View.alert(store:state:action:) instead." -// ) -// @available( -// tvOS, -// introduced: 13, -// deprecated: 100000, -// message: "use `View.alert(store:state:action:) instead." -// ) -// @available( -// watchOS, -// introduced: 6, -// deprecated: 100000, -// message: "use `View.alert(store:state:action:) instead." -// ) -// #if swift(<5.10) -// @MainActor(unsafe) -// #else -// @preconcurrency@MainActor -// #endif -// public func legacyAlert( -// store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> AlertState?, -// action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action -// ) -> some View { -// self.presentation( -// store: store, state: toDestinationState, action: fromDestinationAction -// ) { `self`, $item, _ in -// let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } -// self.alert(item: $item) { _ in -// Alert(alertState!) { action in -// if let action { -// store.send(.presented(fromDestinationAction(action))) -// } else { -// store.send(.dismiss) -// } -// } -// } -// } -// } -//} +import SwiftUI + +extension View { + /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it + /// becomes `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + @available(iOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") + @available( + macOS, introduced: 10.15, deprecated: 100000, message: "use `View.alert(store:) instead." + ) + @available(tvOS, introduced: 13, deprecated: 100000, message: "use `View.alert(store:) instead.") + @available( + watchOS, introduced: 6, deprecated: 100000, message: "use `View.alert(store:) instead." + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func legacyAlert( + store: Store>, PresentationAction> + ) -> some View { + self.legacyAlert(store: store, state: { $0 }, action: { $0 }) + } + + /// Displays a legacy alert when the store's state becomes non-`nil`, and dismisses it when it + /// becomes `nil`. + /// + /// - Parameters: + /// - store: A store that is focused on ``PresentationState`` and ``PresentationAction`` for an + /// alert. + /// - toDestinationState: A transformation to extract alert state from the presentation state. + /// - fromDestinationAction: A transformation to embed alert actions into the presentation + /// action. + @available( + iOS, + introduced: 13, + deprecated: 100000, + message: "use `View.alert(store:state:action:) instead." + ) + @available( + macOS, + introduced: 10.15, + deprecated: 100000, + message: "use `View.alert(store:state:action:) instead." + ) + @available( + tvOS, + introduced: 13, + deprecated: 100000, + message: "use `View.alert(store:state:action:) instead." + ) + @available( + watchOS, + introduced: 6, + deprecated: 100000, + message: "use `View.alert(store:state:action:) instead." + ) + #if swift(<5.10) + @MainActor(unsafe) + #else + @preconcurrency@MainActor + #endif + public func legacyAlert( + store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> AlertState?, + action fromDestinationAction: @escaping (_ alertAction: ButtonAction) -> Action + ) -> some View { + self.presentation( + store: store, state: toDestinationState, action: fromDestinationAction + ) { `self`, $item, _ in + let alertState = store.withState { $0.wrappedValue.flatMap(toDestinationState) } + self.alert(item: $item) { _ in + Alert(alertState!) { action in + if let action { + store.send(.presented(fromDestinationAction(action))) + } else { + store.send(.dismiss) + } + } + } + } + } +} From 052841e8df24ed490669bf14e25372067988d9a0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 15:18:01 -0700 Subject: [PATCH 17/26] wip --- .../Deprecated/NavigationLinkStore.swift | 405 ++++++++++-------- .../SwiftUI/PresentationModifier.swift | 2 +- 2 files changed, 218 insertions(+), 189 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift b/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift index 8de993e7e722..283b7165d8b3 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Deprecated/NavigationLinkStore.swift @@ -1,188 +1,217 @@ -//import SwiftUI -// -///// A view that controls a navigation presentation. -///// -///// This view is similar to SwiftUI's `NavigationLink`, but it allows driving navigation from an -///// optional or enum instead of just a boolean. -///// -///// Typically you use this view by first modeling your features as having a parent feature that -///// holds onto an optional piece of child state using the ``PresentationState``, -///// ``PresentationAction`` and ``Reducer/ifLet(_:action:destination:fileID:filePath:line:column:)-4ub6q`` tools (see -///// for more information). Then in the view you can construct a -///// `NavigationLinkStore` by passing a ``Store`` that is focused on the presentation domain: -///// -///// ```swift -///// NavigationLinkStore( -///// self.store.scope(state: \.$child, action: \.child) -///// ) { -///// viewStore.send(.linkTapped) -///// } destination: { store in -///// ChildView(store: store) -///// } label: { -///// Text("Go to child") -///// } -///// ``` -///// -///// Then when the `child` state flips from `nil` to non-`nil` a drill-down animation will occur to -///// the child domain. -//@available(iOS, introduced: 13, deprecated: 16) -//@available(macOS, introduced: 10.15, deprecated: 13) -//@available(tvOS, introduced: 13, deprecated: 16) -//@available(watchOS, introduced: 6, deprecated: 9) -//public struct NavigationLinkStore< -// State, -// Action, -// DestinationState, -// DestinationAction, -// Destination: View, -// Label: View -//>: View { -// let store: Store, PresentationAction> -// @ObservedObject var viewStore: ViewStore> -// let toDestinationState: (State) -> DestinationState? -// let fromDestinationAction: (DestinationAction) -> Action -// let onTap: () -> Void -// let destination: (Store) -> Destination -// let label: Label -// var isDetailLink = true -// -// public init( -// _ store: Store, PresentationAction>, -// onTap: @escaping () -> Void, -// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, -// @ViewBuilder label: () -> Label -// ) where State == DestinationState, Action == DestinationAction { -// self.init( -// store, -// state: { $0 }, -// action: { $0 }, -// onTap: onTap, -// destination: destination, -// label: label -// ) -// } -// -// public init( -// _ store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// onTap: @escaping () -> Void, -// @ViewBuilder destination: @escaping (_ store: Store) -> -// Destination, -// @ViewBuilder label: () -> Label -// ) { -// let store = store.scope( -// id: nil, -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0.wrappedValue.flatMap(toDestinationState) == nil } -// ) -// self.store = store -// self.viewStore = ViewStore( -// store.scope( -// id: nil, -// state: ToState { $0.wrappedValue.flatMap(toDestinationState) != nil }, -// action: { $0 }, -// isInvalid: nil -// ), -// observe: { $0 } -// ) -// self.toDestinationState = toDestinationState -// self.fromDestinationAction = fromDestinationAction -// self.onTap = onTap -// self.destination = destination -// self.label = label() -// } -// -// public init( -// _ store: Store, PresentationAction>, -// id: State.ID, -// onTap: @escaping () -> Void, -// @ViewBuilder destination: @escaping (_ store: Store) -> Destination, -// @ViewBuilder label: () -> Label -// ) where State == DestinationState, Action == DestinationAction, State: Identifiable { -// self.init( -// store, -// state: { $0 }, -// action: { $0 }, -// id: id, -// onTap: onTap, -// destination: destination, -// label: label -// ) -// } -// -// public init( -// _ store: Store, PresentationAction>, -// state toDestinationState: @escaping (_ state: State) -> DestinationState?, -// action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, -// id: DestinationState.ID, -// onTap: @escaping () -> Void, -// @ViewBuilder destination: @escaping (_ store: Store) -> -// Destination, -// @ViewBuilder label: () -> Label -// ) where DestinationState: Identifiable { -// let store = store.scope( -// id: nil, -// state: ToState(\.self), -// action: { $0 }, -// isInvalid: { $0.wrappedValue.flatMap(toDestinationState)?.id != id } -// ) -// self.store = store -// self.viewStore = ViewStore( -// store.scope( -// id: nil, -// state: ToState { $0.wrappedValue.flatMap(toDestinationState)?.id == id }, -// action: { $0 }, -// isInvalid: nil -// ), -// observe: { $0 } -// ) -// self.toDestinationState = toDestinationState -// self.fromDestinationAction = fromDestinationAction -// self.onTap = onTap -// self.destination = destination -// self.label = label() -// } -// -// public var body: some View { -// NavigationLink( -// isActive: Binding( -// get: { self.viewStore.state }, -// set: { -// if $0 { -// withTransaction($1, self.onTap) -// } else if self.viewStore.state { -// self.viewStore.send(.dismiss, transaction: $1) -// } -// } -// ) -// ) { -// IfLetStore( -// self.store.scope( -// id: nil, -// state: ToState( -// returningLastNonNilValue { $0.wrappedValue.flatMap(self.toDestinationState) } -// ), -// action: { .presented(self.fromDestinationAction($0)) }, -// isInvalid: nil -// ), -// then: self.destination -// ) -// } label: { -// self.label -// } -// #if os(iOS) -// .isDetailLink(self.isDetailLink) -// #endif -// } -// -// @available(macOS, unavailable) -// @available(tvOS, unavailable) -// @available(watchOS, unavailable) -// public func isDetailLink(_ isDetailLink: Bool) -> Self { -// var link = self -// link.isDetailLink = isDetailLink -// return link -// } -//} +import Combine +import SwiftUI + +/// A view that controls a navigation presentation. +/// +/// This view is similar to SwiftUI's `NavigationLink`, but it allows driving navigation from an +/// optional or enum instead of just a boolean. +/// +/// Typically you use this view by first modeling your features as having a parent feature that +/// holds onto an optional piece of child state using the ``PresentationState``, +/// ``PresentationAction`` and ``Reducer/ifLet(_:action:destination:fileID:filePath:line:column:)-4ub6q`` tools (see +/// for more information). Then in the view you can construct a +/// `NavigationLinkStore` by passing a ``Store`` that is focused on the presentation domain: +/// +/// ```swift +/// NavigationLinkStore( +/// self.store.scope(state: \.$child, action: \.child) +/// ) { +/// viewStore.send(.linkTapped) +/// } destination: { store in +/// ChildView(store: store) +/// } label: { +/// Text("Go to child") +/// } +/// ``` +/// +/// Then when the `child` state flips from `nil` to non-`nil` a drill-down animation will occur to +/// the child domain. +@available(iOS, introduced: 13, deprecated: 16) +@available(macOS, introduced: 10.15, deprecated: 13) +@available(tvOS, introduced: 13, deprecated: 16) +@available(watchOS, introduced: 6, deprecated: 9) +public struct NavigationLinkStore< + State, + Action, + DestinationState, + DestinationAction, + Destination: View, + Label: View +>: View { + let store: Store, PresentationAction> + @ObservedObject var viewStore: ViewStore> + let toDestinationState: (State) -> DestinationState? + let fromDestinationAction: (DestinationAction) -> Action + let onTap: () -> Void + let destination: (Store) -> Destination + let label: Label + var isDetailLink = true + + public init( + _ store: Store, PresentationAction>, + onTap: @escaping () -> Void, + @ViewBuilder destination: @escaping (_ store: Store) -> Destination, + @ViewBuilder label: () -> Label + ) where State == DestinationState, Action == DestinationAction { + self.init( + store, + state: { $0 }, + action: { $0 }, + onTap: onTap, + destination: destination, + label: label + ) + } + + public init( + _ store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + onTap: @escaping () -> Void, + @ViewBuilder destination: @escaping (_ store: Store) -> + Destination, + @ViewBuilder label: () -> Label + ) { + func open( + _ core: some Core, PresentationAction> + ) -> any Core, PresentationAction> { + PresentationCore(base: core, toDestinationState: toDestinationState) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + self.store = store + self.viewStore = ViewStore( + store._scope( + state: { $0.wrappedValue.flatMap(toDestinationState) != nil }, + action: { $0 } + ), + observe: { $0 } + ) + self.toDestinationState = toDestinationState + self.fromDestinationAction = fromDestinationAction + self.onTap = onTap + self.destination = destination + self.label = label() + } + + public init( + _ store: Store, PresentationAction>, + id: State.ID, + onTap: @escaping () -> Void, + @ViewBuilder destination: @escaping (_ store: Store) -> Destination, + @ViewBuilder label: () -> Label + ) where State == DestinationState, Action == DestinationAction, State: Identifiable { + self.init( + store, + state: { $0 }, + action: { $0 }, + id: id, + onTap: onTap, + destination: destination, + label: label + ) + } + + public init( + _ store: Store, PresentationAction>, + state toDestinationState: @escaping (_ state: State) -> DestinationState?, + action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action, + id: DestinationState.ID, + onTap: @escaping () -> Void, + @ViewBuilder destination: @escaping (_ store: Store) -> + Destination, + @ViewBuilder label: () -> Label + ) where DestinationState: Identifiable { + func open( + _ core: some Core, PresentationAction> + ) -> any Core, PresentationAction> { + NavigationLinkCore(base: core, id: id, toDestinationState: toDestinationState) + } + let store = store.scope( + id: store.id(state: \.self, action: \.self), + childCore: open(store.core) + ) + self.store = store + self.viewStore = ViewStore( + store._scope( + state: { $0.wrappedValue.flatMap(toDestinationState)?.id == id }, + action: { $0 } + ), + observe: { $0 } + ) + self.toDestinationState = toDestinationState + self.fromDestinationAction = fromDestinationAction + self.onTap = onTap + self.destination = destination + self.label = label() + } + + public var body: some View { + NavigationLink( + isActive: Binding( + get: { self.viewStore.state }, + set: { + if $0 { + withTransaction($1, self.onTap) + } else if self.viewStore.state { + self.viewStore.send(.dismiss, transaction: $1) + } + } + ) + ) { + IfLetStore( + self.store._scope( + state: returningLastNonNilValue { $0.wrappedValue.flatMap(self.toDestinationState) }, + action: { .presented(self.fromDestinationAction($0)) } + ), + then: self.destination + ) + } label: { + self.label + } + #if os(iOS) + .isDetailLink(self.isDetailLink) + #endif + } + + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + public func isDetailLink(_ isDetailLink: Bool) -> Self { + var link = self + link.isDetailLink = isDetailLink + return link + } +} + +private final class NavigationLinkCore< + Base: Core, PresentationAction>, + State, + Action, + DestinationState: Identifiable +>: Core { + let base: Base + let id: DestinationState.ID + let toDestinationState: (State) -> DestinationState? + init( + base: Base, + id: DestinationState.ID, + toDestinationState: @escaping (State) -> DestinationState? + ) { + self.base = base + self.id = id + self.toDestinationState = toDestinationState + } + var state: Base.State { + base.state + } + func send(_ action: Base.Action) -> Task? { + base.send(action) + } + var canStoreCacheChildren: Bool { base.canStoreCacheChildren } + var didSet: CurrentValueRelay { base.didSet } + var isInvalid: Bool { state.wrappedValue.flatMap(toDestinationState)?.id != id || base.isInvalid } + var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift index 85f19027e1da..0bf535b27471 100644 --- a/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift +++ b/Sources/ComposableArchitecture/SwiftUI/PresentationModifier.swift @@ -326,7 +326,7 @@ public struct PresentationStore< } } -private final class PresentationCore< +final class PresentationCore< Base: Core, PresentationAction>, State, Action, From e34e628dddb5f0b8bad5a7abf1263c1ea9d4e3e2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 16:22:29 -0700 Subject: [PATCH 18/26] wip --- Sources/ComposableArchitecture/Core.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index baa0173aae57..2e3d0199f486 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -181,7 +181,7 @@ final class RootCore: Core { private actor DefaultIsolation {} } -class ScopedCore: Core { +final class ScopedCore: Core { var base: Base let stateKeyPath: KeyPath let actionKeyPath: CaseKeyPath @@ -214,11 +214,12 @@ class ScopedCore: Core { } } -class IfLetCore: Core { +final class IfLetCore: Core { var base: Base var cachedState: State let stateKeyPath: KeyPath let actionKeyPath: CaseKeyPath + var parentCancellable: AnyCancellable? init( base: Base, cachedState: State, @@ -231,11 +232,13 @@ class IfLetCore: Core { self.actionKeyPath = actionKeyPath } var state: State { - base.state[keyPath: stateKeyPath] ?? cachedState + let state = base.state[keyPath: stateKeyPath] ?? cachedState + cachedState = state + return state } func send(_ action: Action) -> Task? { #if DEBUG - if BindingLocal.isActive && base.state[keyPath: stateKeyPath] == nil { + if BindingLocal.isActive && isInvalid { return nil } #endif @@ -255,7 +258,7 @@ class IfLetCore: Core { } } -class ClosureScopedCore: Core { +final class ClosureScopedCore: Core { var base: Base let toState: (Base.State) -> State let fromAction: (Action) -> Base.Action From 712fa8dc132961ffd7aeacfca4360f0b77c791af Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 13 Sep 2024 23:04:20 -0700 Subject: [PATCH 19/26] wip --- Sources/ComposableArchitecture/Core.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index 2e3d0199f486..1a466b616d5a 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -182,7 +182,7 @@ final class RootCore: Core { } final class ScopedCore: Core { - var base: Base + let base: Base let stateKeyPath: KeyPath let actionKeyPath: CaseKeyPath init( @@ -215,7 +215,7 @@ final class ScopedCore: Core { } final class IfLetCore: Core { - var base: Base + let base: Base var cachedState: State let stateKeyPath: KeyPath let actionKeyPath: CaseKeyPath @@ -259,7 +259,7 @@ final class IfLetCore: Core { } final class ClosureScopedCore: Core { - var base: Base + let base: Base let toState: (Base.State) -> State let fromAction: (Action) -> Base.Action init( From d9eb1f67410219a9c5222c052853733b60204f73 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 14 Sep 2024 09:37:25 -0700 Subject: [PATCH 20/26] wip --- Sources/ComposableArchitecture/Core.swift | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index 1a466b616d5a..ddd3ae29fde5 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -22,9 +22,15 @@ final class InvalidCore: Core { } func send(_ action: Action) -> Task? { nil } + @inlinable + @inline(__always) var canStoreCacheChildren: Bool { false } let didSet = CurrentValueRelay(()) + @inlinable + @inline(__always) var isInvalid: Bool { true } + @inlinable + @inline(__always) var effectCancellables: [UUID: AnyCancellable] { [:] } } @@ -36,8 +42,12 @@ final class RootCore: Core { } let reducer: Root + @inlinable + @inline(__always) var canStoreCacheChildren: Bool { true } let didSet = CurrentValueRelay(()) + @inlinable + @inline(__always) var isInvalid: Bool { false } private var bufferedActions: [Root.Action] = [] @@ -194,21 +204,33 @@ final class ScopedCore: Core { self.stateKeyPath = stateKeyPath self.actionKeyPath = actionKeyPath } + @inlinable + @inline(__always) var state: State { base.state[keyPath: stateKeyPath] } + @inlinable + @inline(__always) func send(_ action: Action) -> Task? { base.send(actionKeyPath(action)) } + @inlinable + @inline(__always) var canStoreCacheChildren: Bool { base.canStoreCacheChildren } + @inlinable + @inline(__always) var didSet: CurrentValueRelay { base.didSet } + @inlinable + @inline(__always) var isInvalid: Bool { base.isInvalid } + @inlinable + @inline(__always) var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } @@ -231,11 +253,15 @@ final class IfLetCore: Core { self.stateKeyPath = stateKeyPath self.actionKeyPath = actionKeyPath } + @inlinable + @inline(__always) var state: State { let state = base.state[keyPath: stateKeyPath] ?? cachedState cachedState = state return state } + @inlinable + @inline(__always) func send(_ action: Action) -> Task? { #if DEBUG if BindingLocal.isActive && isInvalid { @@ -244,15 +270,23 @@ final class IfLetCore: Core { #endif return base.send(actionKeyPath(action)) } + @inlinable + @inline(__always) var canStoreCacheChildren: Bool { base.canStoreCacheChildren } + @inlinable + @inline(__always) var didSet: CurrentValueRelay { base.didSet } + @inlinable + @inline(__always) var isInvalid: Bool { base.state[keyPath: stateKeyPath] == nil || base.isInvalid } + @inlinable + @inline(__always) var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } @@ -271,21 +305,33 @@ final class ClosureScopedCore: Core { self.toState = toState self.fromAction = fromAction } + @inlinable + @inline(__always) var state: State { toState(base.state) } + @inlinable + @inline(__always) func send(_ action: Action) -> Task? { base.send(fromAction(action)) } + @inlinable + @inline(__always) var canStoreCacheChildren: Bool { false } + @inlinable + @inline(__always) var didSet: CurrentValueRelay { base.didSet } + @inlinable + @inline(__always) var isInvalid: Bool { base.isInvalid } + @inlinable + @inline(__always) var effectCancellables: [UUID: AnyCancellable] { base.effectCancellables } From 62a61c3367dc0ed88d5c5ddbe5110898bda986d7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 27 Sep 2024 15:57:56 -0400 Subject: [PATCH 21/26] get rid of originating action --- Sources/ComposableArchitecture/Core.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index ddd3ae29fde5..006ab1c16317 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -62,10 +62,10 @@ final class RootCore: Core { } func send(_ action: Root.Action) -> Task? { _withoutPerceptionChecking { - send(action, originatingFrom: nil) + _send(action) } } - func send(_ action: Root.Action, originatingFrom originatingAction: Any?) -> Task? { + func _send(_ action: Root.Action) -> Task? { self.bufferedActions.append(action) guard !self.isSending else { return nil } @@ -80,8 +80,7 @@ final class RootCore: Core { self.isSending = false if !self.bufferedActions.isEmpty { if let task = self.send( - self.bufferedActions.removeLast(), - originatingFrom: originatingAction + self.bufferedActions.removeLast() ) { tasks.withValue { $0.append(task) } } @@ -114,7 +113,7 @@ final class RootCore: Core { receiveValue: { [weak self] effectAction in guard let self else { return } if let task = continuation.yield({ - self.send(effectAction, originatingFrom: action) + self.send(effectAction) }) { tasks.withValue { $0.append(task) } } @@ -159,7 +158,7 @@ final class RootCore: Core { ) } if let task = continuation.yield({ - self.send(effectAction, originatingFrom: action) + self.send(effectAction) }) { tasks.withValue { $0.append(task) } } From c70426fcb268a057e12f812bb7cf8a186396bc79 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 2 Oct 2024 09:42:36 -0700 Subject: [PATCH 22/26] wip --- .../Observation/NavigationStack+Observation.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index a9ade7c60014..500e3b03658a 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -192,6 +192,7 @@ public struct _NavigationDestinationViewModifier< } } + @MainActor private func navigationDestination(component: StackState.Component) -> Destination { let id = store.id( state: From 1b7f72cac0cbd475ba88ada352d2a5481734429d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Feb 2025 23:39:15 -0800 Subject: [PATCH 23/26] wip --- Tests/ComposableArchitectureTests/StoreLifetimeTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index be246622091c..92ff21294242 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -140,7 +140,7 @@ final class StoreLifetimeTests: BaseTCATestCase { child.send(.start) XCTAssertEqual(store.withState(\.child.count), 1) } - await clock.run() + await clock.run(timeout: .seconds(5)) XCTAssertEqual(store.withState(\.child.count), 2) } } From 95570eb6020445a876e31d9596a7149c89ee70c7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 13 Mar 2025 10:37:23 -0700 Subject: [PATCH 24/26] Deprecate non-writable scopes --- .../IdentifiedArray+Observation.swift | 35 ++- .../NavigationStack+Observation.swift | 97 +++++++ .../Observation/Store+Observation.swift | 247 ++++++++++++++++++ Sources/ComposableArchitecture/Store.swift | 18 ++ 4 files changed, 393 insertions(+), 4 deletions(-) diff --git a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift index d2e912a98077..f5955233ba98 100644 --- a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift @@ -67,7 +67,34 @@ extension Store where State: ObservableState { /// - filePath: The filePath. /// - line: The line. /// - Returns: An collection of stores of child state. - @_disfavoredOverload + public func scope( + state: WritableKeyPath>, + action: CaseKeyPath>, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) -> some RandomAccessCollection> { + if !core.canStoreCacheChildren { + reportIssue( + uncachedStoreWarning(self), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } + return _StoreCollection(self.scope(state: state, action: action)) + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) public func scope( state: KeyPath>, action: CaseKeyPath>, @@ -96,7 +123,7 @@ public struct _StoreCollection: RandomAc #if swift(<5.10) @MainActor(unsafe) #else - @preconcurrency@MainActor + @preconcurrency @MainActor #endif fileprivate init(_ store: Store, IdentifiedAction>) { self.store = store @@ -135,8 +162,8 @@ public struct _StoreCollection: RandomAc IfLetCore( base: core, cachedState: self.data[position], - stateKeyPath: \.[id:elementID], - actionKeyPath: \.[id:elementID] + stateKeyPath: \.[id: elementID], + actionKeyPath: \.[id: elementID] ) } return self.store.scope(id: scopeID, childCore: open(self.store.core)) diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index f998513643c7..04fac3fa4074 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -61,6 +61,27 @@ extension Binding { #else @MainActor(unsafe) #endif + public func scope( + state: WritableKeyPath>, + action: CaseKeyPath> + ) -> Binding, StackAction>> + where Value == Store { + self[state: state, action: action] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -81,6 +102,27 @@ extension SwiftUI.Bindable { #else @MainActor(unsafe) #endif + public func scope( + state: WritableKeyPath>, + action: CaseKeyPath> + ) -> Binding, StackAction>> + where Value == Store { + self[state: state, action: action] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -99,6 +141,22 @@ extension Perception.Bindable { /// /// See ``SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:)`` defined on `Binding` for more /// information. + public func scope( + state: WritableKeyPath>, + action: CaseKeyPath> + ) -> Binding, StackAction>> + where Value == Store { + self[state: state, action: action] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) public func scope( state: KeyPath>, action: CaseKeyPath> @@ -118,6 +176,27 @@ extension UIBindable { #else @MainActor(unsafe) #endif + public func scope( + state: WritableKeyPath>, + action: CaseKeyPath> + ) -> UIBinding, StackAction>> + where Value == Store { + self[state: state, action: action] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -384,6 +463,24 @@ public struct _NavigationLinkStoreContent: View { } extension Store where State: ObservableState { + fileprivate subscript( + state state: WritableKeyPath>, + action action: CaseKeyPath>, + isInViewBody isInViewBody: Bool = _isInPerceptionTracking + ) -> Store, StackAction> { + get { + #if DEBUG && !os(visionOS) + _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { + self.scope(state: state, action: action) + } + #else + self.scope(state: state, action: action) + #endif + } + set {} + } + + @available(*, deprecated) fileprivate subscript( state state: KeyPath>, action action: CaseKeyPath>, diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index de5b3db45a29..8b5964c1525e 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -80,6 +80,48 @@ extension Store where State: ObservableState { /// - line: The source `#line` associated with the scoping. /// - column: The source `#column` associated with the scoping. /// - Returns: An optional store of non-optional child state and actions. + public func scope( + state stateKeyPath: WritableKeyPath, + action actionKeyPath: CaseKeyPath, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) -> Store? { + if !core.canStoreCacheChildren { + reportIssue( + uncachedStoreWarning(self), + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } + let id = id(state: stateKeyPath, action: actionKeyPath) + guard let childState = state[keyPath: stateKeyPath] + else { + children[id] = nil // TODO: Eager? + return nil + } + func open(_ core: some Core) -> any Core { + IfLetCore( + base: core, + cachedState: childState, + stateKeyPath: stateKeyPath, + actionKeyPath: actionKeyPath + ) + } + return scope(id: id, childCore: open(core)) + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) public func scope( state stateKeyPath: KeyPath, action actionKeyPath: CaseKeyPath, @@ -171,6 +213,40 @@ extension Binding { #else @MainActor(unsafe) #endif + public func scope( + state: WritableKeyPath, + action: CaseKeyPath>, + fileID: StaticString = #fileID, + filePath: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> Binding?> + where Value == Store { + self[ + id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), + state: state, + action: action, + isInViewBody: _isInPerceptionTracking, + fileID: _HashableStaticString(rawValue: fileID), + filePath: _HashableStaticString(rawValue: filePath), + line: line, + column: column + ] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif public func scope( state: KeyPath, action: CaseKeyPath>, @@ -250,6 +326,40 @@ extension SwiftUI.Bindable { #else @MainActor(unsafe) #endif + public func scope( + state: WritableKeyPath, + action: CaseKeyPath>, + fileID: StaticString = #fileID, + filePath: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> Binding?> + where Value == Store { + self[ + id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), + state: state, + action: action, + isInViewBody: _isInPerceptionTracking, + fileID: _HashableStaticString(rawValue: fileID), + filePath: _HashableStaticString(rawValue: filePath), + line: line, + column: column + ] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif public func scope( state: KeyPath, action: CaseKeyPath>, @@ -327,6 +437,35 @@ extension Perception.Bindable { /// - line: The line. /// - column: The column. /// - Returns: A binding of an optional child store. + public func scope( + state: WritableKeyPath, + action: CaseKeyPath>, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) -> Binding?> + where Value == Store { + self[ + id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), + state: state, + action: action, + isInViewBody: _isInPerceptionTracking, + fileID: _HashableStaticString(rawValue: fileID), + filePath: _HashableStaticString(rawValue: filePath), + line: line, + column: column + ] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) public func scope( state: KeyPath, action: CaseKeyPath>, @@ -350,6 +489,40 @@ extension Perception.Bindable { } extension UIBindable { + #if swift(>=5.10) + @preconcurrency@MainActor + #else + @MainActor(unsafe) + #endif + public func scope( + state: WritableKeyPath, + action: CaseKeyPath>, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) -> UIBinding?> + where Value == Store { + self[ + id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), + state: state, + action: action, + isInViewBody: _isInPerceptionTracking, + fileID: _HashableStaticString(rawValue: fileID), + filePath: _HashableStaticString(rawValue: filePath), + line: line, + column: column + ] + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) #if swift(>=5.10) @preconcurrency@MainActor #else @@ -379,6 +552,80 @@ extension UIBindable { extension Store where State: ObservableState { @_spi(Internals) + public subscript( + id id: AnyHashable?, + state state: WritableKeyPath, + action action: CaseKeyPath>, + isInViewBody isInViewBody: Bool, + fileID fileID: _HashableStaticString, + filePath filePath: _HashableStaticString, + line line: UInt, + column column: UInt + ) -> Store? { + get { + #if DEBUG && !os(visionOS) + _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { + self.scope( + state: state, + action: action.appending(path: \.presented), + fileID: fileID.rawValue, + filePath: filePath.rawValue, + line: line, + column: column + ) + } + #else + self.scope( + state: state, + action: action.appending(path: \.presented), + fileID: fileID.rawValue, + filePath: filePath.rawValue, + line: line, + column: column + ) + #endif + } + set { + if newValue == nil, + let childState = self.state[keyPath: state], + id == _identifiableID(childState), + !self.core.isInvalid + { + self.send(action(.dismiss)) + if self.state[keyPath: state] != nil { + reportIssue( + """ + A binding at "\(fileID):\(line)" was set to "nil", but the store destination wasn't \ + nil'd out. + + This usually means an "ifLet" has not been integrated with the reducer powering the \ + store, and this reducer is responsible for handling presentation actions. + + To fix this, ensure that "ifLet" is invoked from the reducer's "body": + + Reduce { state, action in + // ... + } + .ifLet(\\.destination, action: \\.destination) { + Destination() + } + + And ensure that every parent reducer is integrated into the root reducer that powers \ + the store. + """, + fileID: fileID.rawValue, + filePath: filePath.rawValue, + line: line, + column: column + ) + return + } + } + } + } + + @_spi(Internals) + @available(*, deprecated) public subscript( id id: AnyHashable?, state state: KeyPath, diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 0c4ffa960d36..2a2df0fdef7a 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -290,6 +290,24 @@ public final class Store { /// - state: A key path from `State` to `ChildState`. /// - action: A case key path from `Action` to `ChildAction`. /// - Returns: A new store with its domain (state and action) transformed. + public func scope( + state: WritableKeyPath, + action: CaseKeyPath + ) -> Store { + func open(_ core: some Core) -> any Core { + ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) + } + return scope(id: id(state: state, action: action), childCore: open(core)) + } + + @available( + *, + deprecated, + message: """ + Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO + """ + ) + @_documentation(visibility: private) public func scope( state: KeyPath, action: CaseKeyPath From 31b07557212dac6c588b11e62324727aede1ce72 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 13 Mar 2025 12:14:38 -0700 Subject: [PATCH 25/26] Revert "Deprecate non-writable scopes" This reverts commit 95570eb6020445a876e31d9596a7149c89ee70c7. --- .../IdentifiedArray+Observation.swift | 35 +-- .../NavigationStack+Observation.swift | 97 ------- .../Observation/Store+Observation.swift | 247 ------------------ Sources/ComposableArchitecture/Store.swift | 18 -- 4 files changed, 4 insertions(+), 393 deletions(-) diff --git a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift index f5955233ba98..d2e912a98077 100644 --- a/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/IdentifiedArray+Observation.swift @@ -67,34 +67,7 @@ extension Store where State: ObservableState { /// - filePath: The filePath. /// - line: The line. /// - Returns: An collection of stores of child state. - public func scope( - state: WritableKeyPath>, - action: CaseKeyPath>, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) -> some RandomAccessCollection> { - if !core.canStoreCacheChildren { - reportIssue( - uncachedStoreWarning(self), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - return _StoreCollection(self.scope(state: state, action: action)) - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) + @_disfavoredOverload public func scope( state: KeyPath>, action: CaseKeyPath>, @@ -123,7 +96,7 @@ public struct _StoreCollection: RandomAc #if swift(<5.10) @MainActor(unsafe) #else - @preconcurrency @MainActor + @preconcurrency@MainActor #endif fileprivate init(_ store: Store, IdentifiedAction>) { self.store = store @@ -162,8 +135,8 @@ public struct _StoreCollection: RandomAc IfLetCore( base: core, cachedState: self.data[position], - stateKeyPath: \.[id: elementID], - actionKeyPath: \.[id: elementID] + stateKeyPath: \.[id:elementID], + actionKeyPath: \.[id:elementID] ) } return self.store.scope(id: scopeID, childCore: open(self.store.core)) diff --git a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift index 04fac3fa4074..f998513643c7 100644 --- a/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift @@ -61,27 +61,6 @@ extension Binding { #else @MainActor(unsafe) #endif - public func scope( - state: WritableKeyPath>, - action: CaseKeyPath> - ) -> Binding, StackAction>> - where Value == Store { - self[state: state, action: action] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -102,27 +81,6 @@ extension SwiftUI.Bindable { #else @MainActor(unsafe) #endif - public func scope( - state: WritableKeyPath>, - action: CaseKeyPath> - ) -> Binding, StackAction>> - where Value == Store { - self[state: state, action: action] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -141,22 +99,6 @@ extension Perception.Bindable { /// /// See ``SwiftUI/Binding/scope(state:action:fileID:filePath:line:column:)`` defined on `Binding` for more /// information. - public func scope( - state: WritableKeyPath>, - action: CaseKeyPath> - ) -> Binding, StackAction>> - where Value == Store { - self[state: state, action: action] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) public func scope( state: KeyPath>, action: CaseKeyPath> @@ -176,27 +118,6 @@ extension UIBindable { #else @MainActor(unsafe) #endif - public func scope( - state: WritableKeyPath>, - action: CaseKeyPath> - ) -> UIBinding, StackAction>> - where Value == Store { - self[state: state, action: action] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif public func scope( state: KeyPath>, action: CaseKeyPath> @@ -463,24 +384,6 @@ public struct _NavigationLinkStoreContent: View { } extension Store where State: ObservableState { - fileprivate subscript( - state state: WritableKeyPath>, - action action: CaseKeyPath>, - isInViewBody isInViewBody: Bool = _isInPerceptionTracking - ) -> Store, StackAction> { - get { - #if DEBUG && !os(visionOS) - _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { - self.scope(state: state, action: action) - } - #else - self.scope(state: state, action: action) - #endif - } - set {} - } - - @available(*, deprecated) fileprivate subscript( state state: KeyPath>, action action: CaseKeyPath>, diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index 8b5964c1525e..de5b3db45a29 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -80,48 +80,6 @@ extension Store where State: ObservableState { /// - line: The source `#line` associated with the scoping. /// - column: The source `#column` associated with the scoping. /// - Returns: An optional store of non-optional child state and actions. - public func scope( - state stateKeyPath: WritableKeyPath, - action actionKeyPath: CaseKeyPath, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) -> Store? { - if !core.canStoreCacheChildren { - reportIssue( - uncachedStoreWarning(self), - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } - let id = id(state: stateKeyPath, action: actionKeyPath) - guard let childState = state[keyPath: stateKeyPath] - else { - children[id] = nil // TODO: Eager? - return nil - } - func open(_ core: some Core) -> any Core { - IfLetCore( - base: core, - cachedState: childState, - stateKeyPath: stateKeyPath, - actionKeyPath: actionKeyPath - ) - } - return scope(id: id, childCore: open(core)) - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) public func scope( state stateKeyPath: KeyPath, action actionKeyPath: CaseKeyPath, @@ -213,40 +171,6 @@ extension Binding { #else @MainActor(unsafe) #endif - public func scope( - state: WritableKeyPath, - action: CaseKeyPath>, - fileID: StaticString = #fileID, - filePath: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> Binding?> - where Value == Store { - self[ - id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), - state: state, - action: action, - isInViewBody: _isInPerceptionTracking, - fileID: _HashableStaticString(rawValue: fileID), - filePath: _HashableStaticString(rawValue: filePath), - line: line, - column: column - ] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif public func scope( state: KeyPath, action: CaseKeyPath>, @@ -326,40 +250,6 @@ extension SwiftUI.Bindable { #else @MainActor(unsafe) #endif - public func scope( - state: WritableKeyPath, - action: CaseKeyPath>, - fileID: StaticString = #fileID, - filePath: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> Binding?> - where Value == Store { - self[ - id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), - state: state, - action: action, - isInViewBody: _isInPerceptionTracking, - fileID: _HashableStaticString(rawValue: fileID), - filePath: _HashableStaticString(rawValue: filePath), - line: line, - column: column - ] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif public func scope( state: KeyPath, action: CaseKeyPath>, @@ -437,35 +327,6 @@ extension Perception.Bindable { /// - line: The line. /// - column: The column. /// - Returns: A binding of an optional child store. - public func scope( - state: WritableKeyPath, - action: CaseKeyPath>, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) -> Binding?> - where Value == Store { - self[ - id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), - state: state, - action: action, - isInViewBody: _isInPerceptionTracking, - fileID: _HashableStaticString(rawValue: fileID), - filePath: _HashableStaticString(rawValue: filePath), - line: line, - column: column - ] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) public func scope( state: KeyPath, action: CaseKeyPath>, @@ -489,40 +350,6 @@ extension Perception.Bindable { } extension UIBindable { - #if swift(>=5.10) - @preconcurrency@MainActor - #else - @MainActor(unsafe) - #endif - public func scope( - state: WritableKeyPath, - action: CaseKeyPath>, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) -> UIBinding?> - where Value == Store { - self[ - id: wrappedValue.currentState[keyPath: state].flatMap(_identifiableID), - state: state, - action: action, - isInViewBody: _isInPerceptionTracking, - fileID: _HashableStaticString(rawValue: fileID), - filePath: _HashableStaticString(rawValue: filePath), - line: line, - column: column - ] - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) #if swift(>=5.10) @preconcurrency@MainActor #else @@ -552,80 +379,6 @@ extension UIBindable { extension Store where State: ObservableState { @_spi(Internals) - public subscript( - id id: AnyHashable?, - state state: WritableKeyPath, - action action: CaseKeyPath>, - isInViewBody isInViewBody: Bool, - fileID fileID: _HashableStaticString, - filePath filePath: _HashableStaticString, - line line: UInt, - column column: UInt - ) -> Store? { - get { - #if DEBUG && !os(visionOS) - _PerceptionLocals.$isInPerceptionTracking.withValue(isInViewBody) { - self.scope( - state: state, - action: action.appending(path: \.presented), - fileID: fileID.rawValue, - filePath: filePath.rawValue, - line: line, - column: column - ) - } - #else - self.scope( - state: state, - action: action.appending(path: \.presented), - fileID: fileID.rawValue, - filePath: filePath.rawValue, - line: line, - column: column - ) - #endif - } - set { - if newValue == nil, - let childState = self.state[keyPath: state], - id == _identifiableID(childState), - !self.core.isInvalid - { - self.send(action(.dismiss)) - if self.state[keyPath: state] != nil { - reportIssue( - """ - A binding at "\(fileID):\(line)" was set to "nil", but the store destination wasn't \ - nil'd out. - - This usually means an "ifLet" has not been integrated with the reducer powering the \ - store, and this reducer is responsible for handling presentation actions. - - To fix this, ensure that "ifLet" is invoked from the reducer's "body": - - Reduce { state, action in - // ... - } - .ifLet(\\.destination, action: \\.destination) { - Destination() - } - - And ensure that every parent reducer is integrated into the root reducer that powers \ - the store. - """, - fileID: fileID.rawValue, - filePath: filePath.rawValue, - line: line, - column: column - ) - return - } - } - } - } - - @_spi(Internals) - @available(*, deprecated) public subscript( id id: AnyHashable?, state state: KeyPath, diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 2a2df0fdef7a..0c4ffa960d36 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -290,24 +290,6 @@ public final class Store { /// - state: A key path from `State` to `ChildState`. /// - action: A case key path from `Action` to `ChildAction`. /// - Returns: A new store with its domain (state and action) transformed. - public func scope( - state: WritableKeyPath, - action: CaseKeyPath - ) -> Store { - func open(_ core: some Core) -> any Core { - ScopedCore(base: core, stateKeyPath: state, actionKeyPath: action) - } - return scope(id: id(state: state, action: action), childCore: open(core)) - } - - @available( - *, - deprecated, - message: """ - Scoped 'state' must be a writable key path. For more information on this change, see the migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.19#TODO - """ - ) - @_documentation(visibility: private) public func scope( state: KeyPath, action: CaseKeyPath From 725189daa78848716cd9bf6421055e17a0ec6925 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 26 Mar 2025 15:56:30 -0700 Subject: [PATCH 26/26] wip --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 637b5a7a87e1..ceedeba3e615 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: Update xcbeautify + run: brew upgrade xcbeautify - name: List available devices run: xcrun simctl list devices available - name: Cache derived data @@ -65,6 +67,8 @@ jobs: - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: Update xcbeautify + run: brew upgrade xcbeautify - name: Install visionOS runtime if: matrix.platform == 'visionOS' run: | @@ -100,6 +104,8 @@ jobs: - uses: actions/checkout@v4 - name: Select Xcode 15.4 run: sudo xcode-select -s /Applications/Xcode_15.4.app + - name: Update xcbeautify + run: brew upgrade xcbeautify - name: Build for library evolution run: make build-for-library-evolution @@ -118,6 +124,8 @@ jobs: deriveddata-examples- - name: Select Xcode 16 run: sudo xcode-select -s /Applications/Xcode_16.2.app + - name: Update xcbeautify + run: brew upgrade xcbeautify - name: Set IgnoreFileSystemDeviceInodeChanges flag run: defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES - name: Update mtime for incremental builds