Files
swift-composable-architectu…/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift
Stephen Celis c373d8eae3 Observable Architecture (#2593)
* 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>
2024-01-26 16:59:03 -08:00

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
)
}
}