Skip to content

Commit e73b36e

Browse files
authored
WithViewStore debug state diffs (#706)
* WithViewStore debug state diffs * More debug checks * fix * wip
1 parent 2d1e21b commit e73b36e

File tree

2 files changed

+165
-116
lines changed

2 files changed

+165
-116
lines changed

Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift

Lines changed: 136 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,79 @@ import SwiftUI
1919
/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
2020
public struct WithViewStore<State, Action, Content> {
2121
private let content: (ViewStore<State, Action>) -> Content
22-
private var prefix: String?
22+
#if DEBUG
23+
private let file: StaticString
24+
private let line: UInt
25+
private var prefix: String?
26+
private var previousState: (State) -> State?
27+
#endif
2328
@ObservedObject private var viewStore: ViewStore<State, Action>
2429

30+
fileprivate init(
31+
store: Store<State, Action>,
32+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
33+
file: StaticString = #fileID,
34+
line: UInt = #line,
35+
content: @escaping (ViewStore<State, Action>) -> Content
36+
) {
37+
self.content = content
38+
#if DEBUG
39+
self.file = file
40+
self.line = line
41+
var previousState: State? = nil
42+
self.previousState = { currentState in
43+
defer { previousState = currentState }
44+
return previousState
45+
}
46+
#endif
47+
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
48+
}
49+
2550
/// Prints debug information to the console whenever the view is computed.
2651
///
2752
/// - Parameter prefix: A string with which to prefix all debug messages.
2853
/// - Returns: A structure that prints debug messages for all computations.
2954
public func debug(_ prefix: String = "") -> Self {
3055
var view = self
31-
view.prefix = prefix
56+
#if DEBUG
57+
view.prefix = prefix
58+
#endif
3259
return view
3360
}
61+
62+
fileprivate var _body: Content {
63+
#if DEBUG
64+
if let prefix = self.prefix {
65+
let difference = self.previousState(self.viewStore.state)
66+
.map {
67+
debugDiff($0, self.viewStore.state).map { "(Changed state)\n\($0)" }
68+
?? "(No difference in state detected)"
69+
}
70+
?? "(Initial state)\n\(debugOutput(self.viewStore.state, indent: 2))"
71+
func typeName(_ type: Any.Type) -> String {
72+
var name = String(reflecting: type)
73+
if let index = name.firstIndex(of: ".") {
74+
name.removeSubrange(...index)
75+
}
76+
return name
77+
}
78+
print(
79+
"""
80+
\(prefix.isEmpty ? "" : "\(prefix): ")\
81+
WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\
82+
@\(self.file):\(self.line) \(difference)
83+
"""
84+
)
85+
}
86+
#endif
87+
return self.content(self.viewStore)
88+
}
3489
}
3590

3691
extension WithViewStore: View where Content: View {
3792
/// Initializes a structure that transforms a store into an observable view store in order to
3893
/// compute views from store state.
39-
94+
///
4095
/// - Parameters:
4196
/// - store: A store.
4297
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
@@ -45,28 +100,25 @@ extension WithViewStore: View where Content: View {
45100
public init(
46101
_ store: Store<State, Action>,
47102
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
103+
file: StaticString = #fileID,
104+
line: UInt = #line,
48105
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
49106
) {
50-
self.content = content
51-
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
107+
self.init(
108+
store: store,
109+
removeDuplicates: isDuplicate,
110+
file: file,
111+
line: line,
112+
content: content
113+
)
52114
}
53115

54116
public var body: Content {
55-
#if DEBUG
56-
if let prefix = self.prefix {
57-
print(
58-
"""
59-
\(prefix.isEmpty ? "" : "\(prefix): ")\
60-
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
61-
"""
62-
)
63-
}
64-
#endif
65-
return self.content(self.viewStore)
117+
self._body
66118
}
67119
}
68120

69-
extension WithViewStore where Content: View, State: Equatable {
121+
extension WithViewStore where State: Equatable, Content: View {
70122
/// Initializes a structure that transforms a store into an observable view store in order to
71123
/// compute views from equatable store state.
72124
///
@@ -75,13 +127,15 @@ extension WithViewStore where Content: View, State: Equatable {
75127
/// - content: A function that can generate content from a view store.
76128
public init(
77129
_ store: Store<State, Action>,
130+
file: StaticString = #fileID,
131+
line: UInt = #line,
78132
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
79133
) {
80-
self.init(store, removeDuplicates: ==, content: content)
134+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
81135
}
82136
}
83137

84-
extension WithViewStore where Content: View, State == Void {
138+
extension WithViewStore where State == Void, Content: View {
85139
/// Initializes a structure that transforms a store into an observable view store in order to
86140
/// compute views from equatable store state.
87141
///
@@ -90,9 +144,11 @@ extension WithViewStore where Content: View, State == Void {
90144
/// - content: A function that can generate content from a view store.
91145
public init(
92146
_ store: Store<State, Action>,
147+
file: StaticString = #fileID,
148+
line: UInt = #line,
93149
@ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content
94150
) {
95-
self.init(store, removeDuplicates: ==, content: content)
151+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
96152
}
97153
}
98154

@@ -104,74 +160,69 @@ extension WithViewStore: DynamicViewContent where State: Collection, Content: Dy
104160
}
105161
}
106162

107-
#if compiler(>=5.3)
108-
import SwiftUI
109-
110-
/// A structure that transforms a store into an observable view store in order to compute scenes
111-
/// from store state.
112-
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
113-
extension WithViewStore: Scene where Content: Scene {
114-
/// Initializes a structure that transforms a store into an observable view store in order to
115-
/// compute scenes from store state.
116-
117-
/// - Parameters:
118-
/// - store: A store.
119-
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
120-
/// equal, repeat view computations are removed,
121-
/// - content: A function that can generate content from a view store.
122-
public init(
123-
_ store: Store<State, Action>,
124-
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
125-
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
126-
) {
127-
self.content = content
128-
self.viewStore = ViewStore(store, removeDuplicates: isDuplicate)
129-
}
163+
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
164+
extension WithViewStore: Scene where Content: Scene {
165+
/// Initializes a structure that transforms a store into an observable view store in order to
166+
/// compute views from store state.
167+
///
168+
/// - Parameters:
169+
/// - store: A store.
170+
/// - isDuplicate: A function to determine when two `State` values are equal. When values are
171+
/// equal, repeat view computations are removed,
172+
/// - content: A function that can generate content from a view store.
173+
public init(
174+
_ store: Store<State, Action>,
175+
removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
176+
file: StaticString = #fileID,
177+
line: UInt = #line,
178+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
179+
) {
180+
self.init(
181+
store: store,
182+
removeDuplicates: isDuplicate,
183+
file: file,
184+
line: line,
185+
content: content
186+
)
187+
}
130188

131-
public var body: Content {
132-
#if DEBUG
133-
if let prefix = self.prefix {
134-
print(
135-
"""
136-
\(prefix.isEmpty ? "" : "\(prefix): ")\
137-
Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body
138-
"""
139-
)
140-
}
141-
#endif
142-
return self.content(self.viewStore)
143-
}
189+
public var body: Content {
190+
self._body
144191
}
192+
}
145193

146-
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
147-
extension WithViewStore where Content: Scene, State: Equatable {
148-
/// Initializes a structure that transforms a store into an observable view store in order to
149-
/// compute views from equatable store state.
150-
///
151-
/// - Parameters:
152-
/// - store: A store of equatable state.
153-
/// - content: A function that can generate content from a view store.
154-
public init(
155-
_ store: Store<State, Action>,
156-
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
157-
) {
158-
self.init(store, removeDuplicates: ==, content: content)
159-
}
194+
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
195+
extension WithViewStore where State: Equatable, Content: Scene {
196+
/// Initializes a structure that transforms a store into an observable view store in order to
197+
/// compute scenes from equatable store state.
198+
///
199+
/// - Parameters:
200+
/// - store: A store of equatable state.
201+
/// - content: A function that can generate content from a view store.
202+
public init(
203+
_ store: Store<State, Action>,
204+
file: StaticString = #fileID,
205+
line: UInt = #line,
206+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
207+
) {
208+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
160209
}
210+
}
161211

162-
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
163-
extension WithViewStore where Content: Scene, State == Void {
164-
/// Initializes a structure that transforms a store into an observable view store in order to
165-
/// compute views from equatable store state.
166-
///
167-
/// - Parameters:
168-
/// - store: A store of equatable state.
169-
/// - content: A function that can generate content from a view store.
170-
public init(
171-
_ store: Store<State, Action>,
172-
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
173-
) {
174-
self.init(store, removeDuplicates: ==, content: content)
175-
}
212+
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
213+
extension WithViewStore where State == Void, Content: Scene {
214+
/// Initializes a structure that transforms a store into an observable view store in order to
215+
/// compute scenes from equatable store state.
216+
///
217+
/// - Parameters:
218+
/// - store: A store of equatable state.
219+
/// - content: A function that can generate content from a view store.
220+
public init(
221+
_ store: Store<State, Action>,
222+
file: StaticString = #fileID,
223+
line: UInt = #line,
224+
@SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
225+
) {
226+
self.init(store, removeDuplicates: ==, file: file, line: line, content: content)
176227
}
177-
#endif
228+
}

Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,36 @@
33
import ComposableArchitecture
44
import SwiftUI
55

6-
#if compiler(>=5.3)
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-
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("+")
30-
}
31-
}
32-
#else
33-
WindowGroup {
34-
EmptyView()
6+
@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
7+
struct TestApp: App {
8+
let store = Store(
9+
initialState: 0,
10+
reducer: Reducer<Int, Void, Void> { state, _, _ in
11+
state += 1
12+
return .none
13+
},
14+
environment: ()
15+
)
16+
17+
var body: some Scene {
18+
WithViewStore(self.store) { viewStore in
19+
#if os(iOS) || os(macOS)
20+
WindowGroup {
21+
EmptyView()
22+
}
23+
.commands {
24+
CommandMenu("Commands") {
25+
Button("Increment") {
26+
viewStore.send(())
3527
}
36-
#endif
28+
.keyboardShortcut("+")
29+
}
3730
}
31+
#else
32+
WindowGroup {
33+
EmptyView()
34+
}
35+
#endif
3836
}
3937
}
40-
#endif
38+
}

0 commit comments

Comments
 (0)