Skip to content

Commit 1b866ec

Browse files
mluisbrownmbrandonw
andcommitted
Conform WithViewStore to Commands (#1113)
* Conform `WithViewStore` to `Commands` * Fix typo * Add coverage for `Commands` * Fix documentation * Conform `WithViewStore` to `AccessibilityRotorContent` and `ToolbarContent` * Conform `WithViewStore` to `TableColumnContent` * Fix indentation * Conform `WithViewStore` to `TableRowContent` * Fix Typo * Conform `WithViewStore` to `DynamicTableRowContent` * Reorder declarations according to the protocol name * Add coverage for `WithViewStore` new conformances * Remove Table-related conformances * Rearrange extensions for better autocomplete. Co-authored-by: Brandon Williams <[email protected]>
1 parent 6ce426f commit 1b866ec

File tree

3 files changed

+265
-34
lines changed

3 files changed

+265
-34
lines changed

Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift

Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public struct WithViewStore<State, Action, Content> {
9393
}
9494
}
9595

96+
// MARK: - View
9697
extension WithViewStore: View where Content: View {
9798
/// Initializes a structure that transforms a store into an observable view store in order to
9899
/// compute views from store state.
@@ -142,7 +143,7 @@ extension WithViewStore where State: Equatable, Content: View {
142143

143144
extension WithViewStore where State == Void, Content: View {
144145
/// Initializes a structure that transforms a store into an observable view store in order to
145-
/// compute views from equatable store state.
146+
/// compute views from void store state.
146147
///
147148
/// - Parameters:
148149
/// - store: A store of equatable state.
@@ -168,11 +169,151 @@ extension WithViewStore: DynamicViewContent where State: Collection, Content: Dy
168169

169170
#if canImport(Combine) && canImport(SwiftUI) && compiler(>=5.3)
170171
import SwiftUI
172+
// MARK: - AccessibilityRotorContent
173+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
174+
extension WithViewStore: AccessibilityRotorContent where Content: AccessibilityRotorContent {
175+
/// Initializes a structure that transforms a store into an observable view store in order to
176+
/// compute accessibility rotor content from store state.
177+
///
178+
/// - Parameters:
179+
/// - store: A store.
180+
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
181+
/// equal, repeat view computations are removed,
182+
/// - content: A function that can generate content from a view store.
183+
public init(
184+
_ store: Store<State, Action>,
185+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
186+
file: StaticString = #fileID,
187+
line: UInt = #line,
188+
@AccessibilityRotorContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
189+
) {
190+
self.init(
191+
store: store,
192+
removeDuplicates: isDuplicate,
193+
file: file,
194+
line: line,
195+
content: content
196+
)
197+
}
198+
public var body: some AccessibilityRotorContent {
199+
self._body
200+
}
201+
}
202+
203+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
204+
extension WithViewStore where State: Equatable, Content: AccessibilityRotorContent {
205+
/// Initializes a structure that transforms a store into an observable view store in order to
206+
/// compute accessibility rotor content from equatable store state.
207+
///
208+
/// - Parameters:
209+
/// - store: A store of equatable state.
210+
/// - content: A function that can generate content from a view store.
211+
public init(
212+
_ store: Store<State, Action>,
213+
file: StaticString = #fileID,
214+
line: UInt = #line,
215+
@AccessibilityRotorContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
216+
) {
217+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
218+
}
219+
}
171220

221+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
222+
extension WithViewStore where State == Void, Content: AccessibilityRotorContent {
223+
/// Initializes a structure that transforms a store into an observable view store in order to
224+
/// compute accessibility rotor content from void store state.
225+
///
226+
/// - Parameters:
227+
/// - store: A store of equatable state.
228+
/// - content: A function that can generate content from a view store.
229+
public init(
230+
_ store: Store<State, Action>,
231+
file: StaticString = #fileID,
232+
line: UInt = #line,
233+
@AccessibilityRotorContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
234+
) {
235+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
236+
}
237+
}
238+
239+
// MARK: - Commands
240+
@available(iOS 14.0, macOS 11.0, *)
241+
@available(tvOS, unavailable)
242+
@available(watchOS, unavailable)
243+
extension WithViewStore: Commands where Content: Commands {
244+
/// Initializes a structure that transforms a store into an observable view store in order to
245+
/// compute commands from store state.
246+
///
247+
/// - Parameters:
248+
/// - store: A store.
249+
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
250+
/// equal, repeat view computations are removed,
251+
/// - content: A function that can generate content from a view store.
252+
public init(
253+
_ store: Store<State, Action>,
254+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
255+
file: StaticString = #fileID,
256+
line: UInt = #line,
257+
@CommandsBuilder content: @escaping (ViewStore<State, Action>) -> Content
258+
) {
259+
self.init(
260+
store: store,
261+
removeDuplicates: isDuplicate,
262+
file: file,
263+
line: line,
264+
content: content
265+
)
266+
}
267+
public var body: some Commands {
268+
self._body
269+
}
270+
}
271+
272+
@available(iOS 14.0, macOS 11.0, *)
273+
@available(tvOS, unavailable)
274+
@available(watchOS, unavailable)
275+
extension WithViewStore where State: Equatable, Content: Commands {
276+
/// Initializes a structure that transforms a store into an observable view store in order to
277+
/// compute commands from equatable store state.
278+
///
279+
/// - Parameters:
280+
/// - store: A store of equatable state.
281+
/// - content: A function that can generate content from a view store.
282+
public init(
283+
_ store: Store<State, Action>,
284+
file: StaticString = #fileID,
285+
line: UInt = #line,
286+
@CommandsBuilder content: @escaping (ViewStore<State, Action>) -> Content
287+
) {
288+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
289+
}
290+
}
291+
292+
@available(iOS 14.0, macOS 11.0, *)
293+
@available(tvOS, unavailable)
294+
@available(watchOS, unavailable)
295+
extension WithViewStore where State == Void, Content: Commands {
296+
/// Initializes a structure that transforms a store into an observable view store in order to
297+
/// compute commands from void store state.
298+
///
299+
/// - Parameters:
300+
/// - store: A store of equatable state.
301+
/// - content: A function that can generate content from a view store.
302+
public init(
303+
_ store: Store<State, Action>,
304+
file: StaticString = #fileID,
305+
line: UInt = #line,
306+
@CommandsBuilder content: @escaping (ViewStore<State, Action>) -> Content
307+
) {
308+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
309+
}
310+
}
311+
312+
// MARK: - Scene
172313
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
173314
extension WithViewStore: Scene where Content: Scene {
174315
/// Initializes a structure that transforms a store into an observable view store in order to
175-
/// compute views from store state.
316+
/// compute scenes from store state.
176317
///
177318
/// - Parameters:
178319
/// - store: A store.
@@ -221,7 +362,7 @@ extension WithViewStore where State: Equatable, Content: Scene {
221362
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
222363
extension WithViewStore where State == Void, Content: Scene {
223364
/// Initializes a structure that transforms a store into an observable view store in order to
224-
/// compute scenes from equatable store state.
365+
/// compute scenes from void store state.
225366
///
226367
/// - Parameters:
227368
/// - store: A store of equatable state.
@@ -236,4 +377,71 @@ extension WithViewStore where State == Void, Content: Scene {
236377
}
237378
}
238379

380+
// MARK: - ToolbarContent
381+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
382+
extension WithViewStore: ToolbarContent where Content: ToolbarContent {
383+
/// Initializes a structure that transforms a store into an observable view store in order to
384+
/// compute toolbar content from store state.
385+
///
386+
/// - Parameters:
387+
/// - store: A store.
388+
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
389+
/// equal, repeat view computations are removed,
390+
/// - content: A function that can generate content from a view store.
391+
public init(
392+
_ store: Store<State, Action>,
393+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
394+
file: StaticString = #fileID,
395+
line: UInt = #line,
396+
@ToolbarContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
397+
) {
398+
self.init(
399+
store: store,
400+
removeDuplicates: isDuplicate,
401+
file: file,
402+
line: line,
403+
content: content
404+
)
405+
}
406+
public var body: some ToolbarContent {
407+
self._body
408+
}
409+
}
410+
411+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
412+
extension WithViewStore where State: Equatable, Content: ToolbarContent {
413+
/// Initializes a structure that transforms a store into an observable view store in order to
414+
/// compute toolbar content from equatable store state.
415+
///
416+
/// - Parameters:
417+
/// - store: A store of equatable state.
418+
/// - content: A function that can generate content from a view store.
419+
public init(
420+
_ store: Store<State, Action>,
421+
file: StaticString = #fileID,
422+
line: UInt = #line,
423+
@ToolbarContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
424+
) {
425+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
426+
}
427+
}
428+
429+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
430+
extension WithViewStore where State == Void, Content: ToolbarContent {
431+
/// Initializes a structure that transforms a store into an observable view store in order to
432+
/// compute toolbar content from void store state.
433+
///
434+
/// - Parameters:
435+
/// - store: A store of equatable state.
436+
/// - content: A function that can generate content from a view store.
437+
public init(
438+
_ store: Store<State, Action>,
439+
file: StaticString = #fileID,
440+
line: UInt = #line,
441+
@ToolbarContentBuilder content: @escaping (ViewStore<State, Action>) -> Content
442+
) {
443+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
444+
}
445+
}
239446
#endif
447+

Tests/ComposableArchitectureTests/TestStoreTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ class TestStoreTests: XCTestCase {
154154
switch action {
155155
case .a:
156156
count += 1
157-
return .merge(.init(value: .b), .init(value: .c), .init(value: .d))
157+
return .merge(Effect(value: .b), Effect(value: .c), Effect(value: .d))
158158
case .b, .c, .d:
159159
count += 1
160160
return .none
Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,63 @@
1-
// NB: This file gathers coverage of `WithViewStore` use as a `Scene`.
1+
// NB: This file gathers coverage of various `WithViewStore` conformances.
22
#if canImport(SwiftUI)
3+
import ComposableArchitecture
4+
import SwiftUI
35

4-
import ComposableArchitecture
5-
import SwiftUI
6+
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
7+
struct TestApp: App {
8+
@Namespace var namespace
9+
10+
let store = Store(
11+
initialState: 0,
12+
reducer: Reducer<Int, Void, Void> { state, _, _ in
13+
state += 1
14+
return .none
15+
},
16+
environment: ()
17+
)
618

7-
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
8-
struct TestApp: App {
9-
let store = Store(
10-
initialState: 0,
11-
reducer: Reducer<Int, Void, Void> { state, _, _ in
12-
state += 1
13-
return .none
14-
},
15-
environment: ()
16-
)
17-
18-
var body: some Scene {
19+
var body: some Scene {
20+
WithViewStore(self.store) { viewStore in
21+
WindowGroup {
22+
checkAccessibilityRotor()
23+
checkToolbar()
24+
}
25+
}
26+
#if os(iOS) || os(macOS)
27+
.commands {
1928
WithViewStore(self.store) { viewStore in
20-
#if os(iOS) || os(macOS)
21-
WindowGroup {
22-
EmptyView()
23-
}
24-
.commands {
25-
CommandMenu("Commands") {
26-
Button("Increment") {
27-
viewStore.send(())
28-
}
29-
.keyboardShortcut("+")
29+
CommandMenu("Commands") {
30+
Button("Increment") {
31+
viewStore.send(())
3032
}
3133
}
32-
#else
33-
WindowGroup {
34-
EmptyView()
34+
}
35+
}
36+
#endif
37+
}
38+
39+
@ViewBuilder
40+
func checkToolbar() -> some View {
41+
Color.clear
42+
.toolbar {
43+
WithViewStore(store) { viewStore in
44+
ToolbarItem {
45+
Button(action: { viewStore.send(()) }, label: { Text("Increment") })
3546
}
36-
#endif
47+
}
3748
}
38-
}
3949
}
50+
51+
@ViewBuilder
52+
func checkAccessibilityRotor() -> some View {
53+
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
54+
Color.clear
55+
.accessibilityRotor("Rotor") {
56+
WithViewStore(store) { viewStore in
57+
AccessibilityRotorEntry("Value: \(viewStore.state)", 0, in: namespace)
58+
}
59+
}
60+
}
61+
}
62+
}
4063
#endif

0 commit comments

Comments
 (0)