Files
swift-composable-architectu…/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift
Max Desiatov 9fbd834664 Add Scene conformance to WithViewStore (#336)
* Add SceneWithViewStore for accessing stores in scenes

This would allow accessing `ViewStore` instances from a `body` definition of a type conforming to `Scene`. It could be useful for conditional rendering of scenes or sending actions from scene commands. Here's an example:

```swift
import ComposableArchitecture
import SwiftUI

@main
struct CommandsApp: App {
  private let store = Store(
    initialState: RootState(),
    reducer: rootReducer,
    environment: .live(rootEnvironment)
  )

  var body: some Scene {
    SceneWithViewStore(store) { viewStore in
      WindowGroup {
        WorkspaceView()
      }.commands {
        CommandGroup(after: CommandGroupPlacement.newItem) {
          Button("Open...") {
            viewStore.send(.open)
          }.keyboardShortcut("o", modifiers: [.command])
        }
      }
    }
  }
}
```

* Avoid building SceneWithViewStore with old Xcode

* Separate Catalina and Big Sur jobs

This allows testing APIs that are only available on Big Sur

* Unify `WithViewStore` and `SceneWithViewStore`
2021-01-11 19:38:09 -05:00

182 lines
6.5 KiB
Swift

import Combine
import SwiftUI
/// A structure that transforms a store into an observable view store in order to compute views from
/// store state.
///
/// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core
/// views provided by SwiftUI. The known problematic views are:
///
/// * If a `GeometryReader` or `ScrollViewReader` is used inside a `WithViewStore` it will not
/// receive state updates correctly. To work around you either need to reorder the views so that
/// the `GeometryReader` or `ScrollViewReader` wraps `WithViewStore`, or, if that is not possible,
/// then you must hold onto an explicit `@ObservedObject var viewStore: ViewStore<State, Action>`
/// in your view in lieu of using this helper (see
/// [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)).
/// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer
/// inside a `WithViewStore` it will behave erratically. To work around you should use the
/// initializer that takes a binding (see
/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
public struct WithViewStore<State, Action, Content> {
private let content: (ViewStore<State, Action>) -> Content
private var prefix: String?
@ObservedObject private var viewStore: ViewStore<State, Action>
/// Prints debug information to the console whenever the view is computed.
///
/// - Parameter prefix: A string with which to prefix all debug messages.
/// - Returns: A structure that prints debug messages for all computations.
public func debug(_ prefix: String = "") -> Self {
var view = self
view.prefix = prefix
return view
}
}
extension WithViewStore: View where Content: View {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from store state.
/// - Parameters:
/// - store: A store.
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
/// equal, repeat view computations are removed,
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.content = content
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
}
public var body: Content {
#if DEBUG
if let prefix = self.prefix {
print(
"""
\(prefix.isEmpty ? "" : "\(prefix): ")\
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
"""
)
}
#endif
return self.content(self.viewStore)
}
}
extension WithViewStore where Content: View, State: Equatable {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
}
extension WithViewStore where Content: View, State == Void {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
}
extension WithViewStore: DynamicViewContent where State: Collection, Content: DynamicViewContent {
public typealias Data = State
public var data: State {
self.viewStore.state
}
}
#if compiler(>=5.3)
import SwiftUI
/// A structure that transforms a store into an observable view store in order to compute scenes from
/// store state.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension WithViewStore: Scene where Content: Scene {
public typealias Body = Content
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute scenes from store state.
/// - Parameters:
/// - store: A store.
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
/// equal, repeat view computations are removed,
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.content = content
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
}
public var body: Content {
#if DEBUG
if let prefix = self.prefix {
print(
"""
\(prefix.isEmpty ? "" : "\(prefix): ")\
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
"""
)
}
#endif
return self.content(self.viewStore)
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension WithViewStore where Content: Scene, State: Equatable {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension WithViewStore where Content: Scene, State == Void {
/// Initializes a structure that transforms a store into an observable view store in order to
/// compute views from equatable store state.
///
/// - Parameters:
/// - store: A store of equatable state.
/// - content: A function that can generate content from a view store.
public init(
_ store: Store<State, Action>,
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
) {
self.init(store, removeDuplicates: ==, content: content)
}
}
#endif