mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-24 12:14:25 +01:00
* bring back view store performance
* wip
* Allow chaining of store bindings
* wip
* Localize ignoring bindings to text field resignation/dismissal
* wip
* fix DiagnosticsError message (#2597)
* store collection
* wip
* wip
* update migration guide
* Add `@Presents` macro for observable presentation
While it would be nice for the `@PresentationState` property wrapper to
"just work" with TCA's upcoming observable tools, sadly that does not
seem to be the case. Adding observation directly to
`@PresentationState`, as we have done with the beta so far, can break
existing projects due to the additional observation. This primarily
manifests itself in projects that present navigation stacks, where the
`@PresentationState` observation can cause the navigation hierarchy to
recompute and trigger SwiftUI bugs.
The best we've come up with so far is introducing a brand new macro that
automatically wraps a property with `@PresentationState` _and_
instruments it with observation.
We're open to other ideas, and we do have future plans to eliminate the
need for a property wrapper or macro at all, but till then this offers a
non-breaking upgrade path!
* fixes
* Observe child store changes
* wip
* wip
* wip
* Fix typo in MigratingTo1.6.md (#2608)
* Rename bindingViewStore argument to store in MigratingTo1.6.md (#2611)
* wip
* Revert "wip"
This reverts commit f221ed0e1a.
* Add `@Presents` macro for observable presentation (#2604)
* Add `@Presents` macro for observable presentation
While it would be nice for the `@PresentationState` property wrapper to
"just work" with TCA's upcoming observable tools, sadly that does not
seem to be the case. Adding observation directly to
`@PresentationState`, as we have done with the beta so far, can break
existing projects due to the additional observation. This primarily
manifests itself in projects that present navigation stacks, where the
`@PresentationState` observation can cause the navigation hierarchy to
recompute and trigger SwiftUI bugs.
The best we've come up with so far is introducing a brand new macro that
automatically wraps a property with `@PresentationState` _and_
instruments it with observation.
We're open to other ideas, and we do have future plans to eliminate the
need for a property wrapper or macro at all, but till then this offers a
non-breaking upgrade path!
* wip
* Fix perception bindings (#2609)
* Fix runtime warning when binding accesses perceptible state.
* Fix runtime warning in SwiftUI bindings.
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* wip
* wip
* fix
* wip
* wip
* wip
* Check observable state identity for presentation state.
* Add willSset/didSet to registrar types.
* clean up @Presents
* clean up
* fix
* Emit observation warnings in escaping contexts like `ForEach` and `sheet` (#2613)
* Fix perception warning in ForEach.
* fix
---------
Co-authored-by: Brandon Williams <mbrandonw@hey.com>
* Introduce @ViewAction(for:) macro. (#2612)
* Add back @ViewAction macro.
* wip
* wip
* wip
* wip
* wip
* clean up
* wip
* wip
* fix migration guide'
* ViewActionable
* wip
* rename
* wip
* wip
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* Introduce @BindableStore for bindings in pre-iOS 17 (#2610)
* Introduce @BindableStore.
* docs
* wip
* wip
* fixc
* wip
* wip
* wip
* wip
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* re-record intergration logs
* wip
* wip
* localize invalid stores to store collection
* Deprecate closure-based `store.scope` operations (#2618)
These uncached operations can be problematic, especially when working
with observation, which often depends on the stable identity of stores.
* document
* Update warning message
* Performance Improvement: Skip perception checks when calling reducers. (#2622)
* Skip perception checks when calling reducers.
* inline withoutPerceptionChecking() for RELEASE
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
---------
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
* Don't show perception warnings in action closures. (#2614)
* Don't show perception warnings in action closures.
* wip
* wip
* wip
* clean up
* wip
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* fix BindableStore + release
* Add docs
* Change associated type names of ViewActionSending (#2629)
* Fix some @ViewAction annoyances.
* wip
* wip
* wip
* wip
* wip
* wip
* fix
* wip
* fixed merge
* Add new view modifiers for observing alerts/dialogs (#2628)
* Add new view modifiers for observing alerts/dialogs
Instead of:
```swift
.alert(store: store.scope(state: \.$alert, action: \.alert))
```
You can now do:
```swift
.alert($store.scope(state: \.alert, action: \.alert))
```
This new modifier is powered by the same store binding scope operation
that can power `sheet(item:)`, etc., and is much lighter weight than the
previous view modifier, which spun up view stores and `WithViewStore`
views.
* wip
* wip
* wip
---------
Co-authored-by: Brandon Williams <mbrandonw@hey.com>
* Fix uncached warning when using Store.ifLet (#2625)
* Fix uncached warning when using Store.ifLet
* wip
* wip
* wip
* wip
* wip
* wip
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* Resolve packages
* Updated scopes
* wip
* wip
* updated binding docs
* adding docs
* clean up
* wip
* wip
* wip
* clean up
* clean up
* clean up
* wip;
* lots of fixes
* update more docs
* fix
* wip
* wip
* Remove ObservationRegistrarWrapper. (#2634)
* Remove ObservationRegistrarWrapper.
* Delete Sources/ComposableArchitecture/Internal/ObservationRegistrarWrapper.swift
---------
Co-authored-by: Stephen Celis <stephen@stephencelis.com>
* more docs
* update docs
* a few more tests
* fix
* wip
* wip
* wip
* Cache data in store collections (#2635)
* fix tutorial highlighting
* wip
* wip
* wip
* wip
* tests for observation of special domain types
* another test
* fix
* wip
* Implement memoization for perception checks (#2630)
* Implement memoization for isInSwiftUIBody
* tidy up
* Perception caching updates (#2649)
* Small updates to perception caching.
* wip
* debug
* some more macro tests
* syncups tutorial beginnings
* wip
* wip
* wip
* wip
* wip
* merge fixes
* wip
* update tests
* fix
* fix
* fix perception checking in store
* rename task local
* delete old test
* deprecate test using old apis
* fix test
* perception tests for store
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Opt out of key path for Store.ifLet
* sync ups
* lots more sync up tutorial
* more sync ups tutorial
* wip
* wip
* wio
* wip
* wip
* wip
* updated references of 1.6 to 1.7
* wip
* no need to force unwrap here
* fixed crash in ForEach with bindings
* more sync ups tutorial
* more sync ups tutorial
* wip
* more sync ups
* wip
* wip
* Better support for observing copies of values (#2650)
* Explore using _modify
* wip
* wip
* wip
* wip
* wip
* wip
* more tests
* wip
* get another failing test for an edge case
* wip
* tests all passing
* flag for determining when new state was created
* wip
* clean up
* wip
* wip
* wip;
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* New test that currently fails.
* wip
* wip
* Update Sources/ComposableArchitectureMacros/PresentsMacro.swift
* wip
* remove redundant attached member attribute
* storage
* cleanup
* more benchmarks and tests
* wip
* wip
* wip
* wip
* update tests
* wip
* wip
---------
Co-authored-by: Brandon Williams <mbrandonw@hey.com>
* wip
* wip
* wip
* swift-format
* fix
* wip
* wip
* wip
* wip
* Perception
* wip
* wip
* clean up shared state
* fix shared state tests
* wip
* add alert test
* wip
* wip
* wip
* wip
* Use transaction in binding
* wip
* wip
* wip
* wip
* wip
* wip
* uikit
* keep references to controllers when presenting so that we can properly dismiss
* change order of features in shared state demo
* wip
* cleanup
* cleanup
* wip
* wip
* wip
* Fix perception checking for effect actions.
* wip
* wip
* wip
* Fix perception checking for effect actions.
* wip
* wip
* remove sync ups tutorial
* wip
* wip
* wip
* wip
* wip
* docs for observe function for uikit
* Add cancellation to observation'
* re-record integration test snapshots
* fixed some todos
* update test
* remove 5.9.2 checks
* wip
* improve docs
* update docs
* updates
* lots of fixes
* more docs
* remove unneeded file;
* wip
* wip
* wip
* update readme and getting started
* wip
* simplify
* migration stuff
* wip
* Update Models.swift
* wip
* wip
* wip
* Update Bindings.md
* wip
* wip
* wip
* wip
* fix
* wip
* wip
* wip
* wip
* wip
Co-authored-by: Kabir Oberai <oberai.kabir@gmail.com>
---------
Co-authored-by: Brandon Williams <mbrandonw@hey.com>
Co-authored-by: hmhv <admin@hmhv.info>
Co-authored-by: Jimmy Prime <jimmylevelup@gmail.com>
Co-authored-by: Michael Pohl <15653162+Mika5652@users.noreply.github.com>
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
Co-authored-by: George Scott <gscott@gekkoto.com>
Co-authored-by: Kabir Oberai <oberai.kabir@gmail.com>
277 lines
13 KiB
Swift
277 lines
13 KiB
Swift
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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-IfLetStore-with-if-let]"
|
|
)
|
|
public struct IfLetStore<State, Action, Content: View>: View {
|
|
private let content: (ViewStore<State?, Action>) -> Content
|
|
private let store: Store<State?, Action>
|
|
|
|
/// 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`.
|
|
public init<IfContent, ElseContent>(
|
|
_ store: Store<State?, Action>,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> IfContent,
|
|
@ViewBuilder else elseContent: () -> ElseContent
|
|
) where Content == _ConditionalContent<IfContent, ElseContent> {
|
|
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`.
|
|
public init<IfContent>(
|
|
_ store: Store<State?, Action>,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> 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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs"
|
|
)
|
|
public init<IfContent, ElseContent>(
|
|
_ store: Store<PresentationState<State>, PresentationAction<Action>>,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> IfContent,
|
|
@ViewBuilder else elseContent: @escaping () -> ElseContent
|
|
) where Content == _ConditionalContent<IfContent, ElseContent> {
|
|
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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://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:\n\nhttps://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs"
|
|
)
|
|
public init<IfContent>(
|
|
_ store: Store<PresentationState<State>, PresentationAction<Action>>,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> 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:\n\nhttps://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs"
|
|
)
|
|
public init<DestinationState, DestinationAction, IfContent, ElseContent>(
|
|
_ store: Store<PresentationState<DestinationState>, PresentationAction<DestinationAction>>,
|
|
state toState: @escaping (_ destinationState: DestinationState) -> State?,
|
|
action fromAction: @escaping (_ action: Action) -> DestinationAction,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> IfContent,
|
|
@ViewBuilder else elseContent: @escaping () -> ElseContent
|
|
) where Content == _ConditionalContent<IfContent, ElseContent> {
|
|
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:\n\nhttps://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Enum-driven-navigation-APIs"
|
|
)
|
|
public init<DestinationState, DestinationAction, IfContent>(
|
|
_ store: Store<PresentationState<DestinationState>, PresentationAction<DestinationAction>>,
|
|
state toState: @escaping (_ destinationState: DestinationState) -> State?,
|
|
action fromAction: @escaping (_ action: Action) -> DestinationAction,
|
|
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> 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
|
|
)
|
|
}
|
|
}
|