Skip to content

Commit 9fbd834

Browse files
authored
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`
1 parent f6c73e6 commit 9fbd834

File tree

2 files changed

+106
-16
lines changed

2 files changed

+106
-16
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@ on:
1010

1111
jobs:
1212
tests:
13-
runs-on: macOS-latest
13+
runs-on: macos-10.15
1414
strategy:
1515
matrix:
1616
xcode:
1717
- 11.3
1818
- 11.7
19-
- 12
19+
steps:
20+
- uses: actions/checkout@v2
21+
- name: Select Xcode ${{ matrix.xcode }}
22+
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
23+
- name: Run tests
24+
run: make test
25+
26+
bigsur-tests:
27+
runs-on: macos-11.0
28+
strategy:
29+
matrix:
30+
xcode:
31+
- 12.3
2032
steps:
2133
- uses: actions/checkout@v2
2234
- name: Select Xcode ${{ matrix.xcode }}

Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,23 @@ import SwiftUI
1717
/// inside a `WithViewStore` it will behave erratically. To work around you should use the
1818
/// initializer that takes a binding (see
1919
/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
20-
public struct WithViewStore<State, Action, Content>: View where Content: View {
20+
public struct WithViewStore<State, Action, Content> {
2121
private let content: (ViewStore<State, Action>) -> Content
2222
private var prefix: String?
2323
@ObservedObject private var viewStore: ViewStore<State, Action>
2424

25+
/// Prints debug information to the console whenever the view is computed.
26+
///
27+
/// - Parameter prefix: A string with which to prefix all debug messages.
28+
/// - Returns: A structure that prints debug messages for all computations.
29+
public func debug(_ prefix: String = "") -> Self {
30+
var view = self
31+
view.prefix = prefix
32+
return view
33+
}
34+
}
35+
36+
extension WithViewStore: View where Content: View {
2537
/// Initializes a structure that transforms a store into an observable view store in order to
2638
/// compute views from store state.
2739

@@ -39,7 +51,7 @@ public struct WithViewStore<State, Action, Content>: View where Content: View {
3951
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
4052
}
4153

42-
public var body: some View {
54+
public var body: Content {
4355
#if DEBUG
4456
if let prefix = self.prefix {
4557
print(
@@ -52,19 +64,9 @@ public struct WithViewStore<State, Action, Content>: View where Content: View {
5264
#endif
5365
return self.content(self.viewStore)
5466
}
55-
56-
/// Prints debug information to the console whenever the view is computed.
57-
///
58-
/// - Parameter prefix: A string with which to prefix all debug messages.
59-
/// - Returns: A structure that prints debug messages for all computations.
60-
public func debug(_ prefix: String = "") -> Self {
61-
var view = self
62-
view.prefix = prefix
63-
return view
64-
}
6567
}
6668

67-
extension WithViewStore where State: Equatable {
69+
extension WithViewStore where Content: View, State: Equatable {
6870
/// Initializes a structure that transforms a store into an observable view store in order to
6971
/// compute views from equatable store state.
7072
///
@@ -79,7 +81,7 @@ extension WithViewStore where State: Equatable {
7981
}
8082
}
8183

82-
extension WithViewStore where State == Void {
84+
extension WithViewStore where Content: View, State == Void {
8385
/// Initializes a structure that transforms a store into an observable view store in order to
8486
/// compute views from equatable store state.
8587
///
@@ -101,3 +103,79 @@ extension WithViewStore: DynamicViewContent where State: Collection, Content: Dy
101103
self.viewStore.state
102104
}
103105
}
106+
107+
#if compiler(>=5.3)
108+
109+
import SwiftUI
110+
111+
/// A structure that transforms a store into an observable view store in order to compute scenes from
112+
/// store state.
113+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
114+
extension WithViewStore: Scene where Content: Scene {
115+
public typealias Body = Content
116+
117+
/// Initializes a structure that transforms a store into an observable view store in order to
118+
/// compute scenes from store state.
119+
120+
/// - Parameters:
121+
/// - store: A store.
122+
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
123+
/// equal, repeat view computations are removed,
124+
/// - content: A function that can generate content from a view store.
125+
public init(
126+
_ store: Store<State, Action>,
127+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
128+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
129+
) {
130+
self.content = content
131+
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
132+
}
133+
134+
public var body: Content {
135+
#if DEBUG
136+
if let prefix = self.prefix {
137+
print(
138+
"""
139+
\(prefix.isEmpty ? "" : "\(prefix): ")\
140+
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
141+
"""
142+
)
143+
}
144+
#endif
145+
return self.content(self.viewStore)
146+
}
147+
}
148+
149+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
150+
extension WithViewStore where Content: Scene, State: Equatable {
151+
/// Initializes a structure that transforms a store into an observable view store in order to
152+
/// compute views from equatable store state.
153+
///
154+
/// - Parameters:
155+
/// - store: A store of equatable state.
156+
/// - content: A function that can generate content from a view store.
157+
public init(
158+
_ store: Store<State, Action>,
159+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
160+
) {
161+
self.init(store, removeDuplicates: ==, content: content)
162+
}
163+
}
164+
165+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
166+
extension WithViewStore where Content: Scene, State == Void {
167+
/// Initializes a structure that transforms a store into an observable view store in order to
168+
/// compute views from equatable store state.
169+
///
170+
/// - Parameters:
171+
/// - store: A store of equatable state.
172+
/// - content: A function that can generate content from a view store.
173+
public init(
174+
_ store: Store<State, Action>,
175+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
176+
) {
177+
self.init(store, removeDuplicates: ==, content: content)
178+
}
179+
}
180+
181+
#endif

0 commit comments

Comments
 (0)