|
1 | 1 | #if canImport(Combine) && canImport(SwiftUI)
|
2 |
| - import Combine |
3 |
| - import SwiftUI |
4 |
| - |
5 |
| - /// A structure that transforms a store into an observable view store in order to compute views from |
6 |
| - /// store state. |
7 |
| - /// |
8 |
| - /// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core |
9 |
| - /// views provided by SwiftUI. The known problematic views are: |
10 |
| - /// |
11 |
| - /// * If a `GeometryReader` or `ScrollViewReader` is used inside a ``WithViewStore`` it will not |
12 |
| - /// receive state updates correctly. To work around you either need to reorder the views so that |
13 |
| - /// the `GeometryReader` or `ScrollViewReader` wraps ``WithViewStore``, or, if that is not |
14 |
| - /// possible, then you must hold onto an explicit |
15 |
| - /// `@ObservedObject var viewStore: ViewStore<State, Action>` in your view in lieu of using this |
16 |
| - /// helper (see [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)). |
17 |
| - /// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer |
18 |
| - /// inside a ``WithViewStore`` it will behave erratically. To work around you should use the |
19 |
| - /// initializer that takes a binding (see |
20 |
| - /// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)). |
| 2 | +import Combine |
| 3 | +import SwiftUI |
| 4 | + |
| 5 | +/// A structure that transforms a store into an observable view store in order to compute views from |
| 6 | +/// store state. |
| 7 | +/// |
| 8 | +/// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core |
| 9 | +/// views provided by SwiftUI. The known problematic views are: |
| 10 | +/// |
| 11 | +/// * If a `GeometryReader` or `ScrollViewReader` is used inside a ``WithViewStore`` it will not |
| 12 | +/// receive state updates correctly. To work around you either need to reorder the views so that |
| 13 | +/// the `GeometryReader` or `ScrollViewReader` wraps ``WithViewStore``, or, if that is not |
| 14 | +/// possible, then you must hold onto an explicit |
| 15 | +/// `@ObservedObject var viewStore: ViewStore<State, Action>` in your view in lieu of using this |
| 16 | +/// helper (see [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)). |
| 17 | +/// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer |
| 18 | +/// inside a ``WithViewStore`` it will behave erratically. To work around you should use the |
| 19 | +/// initializer that takes a binding (see |
| 20 | +/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)). |
21 | 21 | ///
|
22 | 22 | /// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core
|
23 | 23 | /// views provided by SwiftUI. The known problematic views are:
|
|
33 | 33 | /// initializer that takes a binding (see
|
34 | 34 | /// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
|
35 | 35 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
36 |
| - public struct WithViewStore<State, Action, Content> { |
37 |
| - private let content: (ViewStore<State, Action>) -> Content |
38 |
| - private var prefix: String? |
39 |
| - @ObservedObject private var viewStore: ViewStore<State, Action> |
40 |
| - |
41 |
| - /// Prints debug information to the console whenever the view is computed. |
42 |
| - /// |
43 |
| - /// - Parameter prefix: A string with which to prefix all debug messages. |
44 |
| - /// - Returns: A structure that prints debug messages for all computations. |
45 |
| - public func debug(_ prefix: String = "") -> Self { |
46 |
| - var view = self |
47 |
| - view.prefix = prefix |
48 |
| - return view |
49 |
| - } |
| 36 | +public struct WithViewStore<State, Action, Content> { |
| 37 | + private let content: (ViewStore<State, Action>) -> Content |
| 38 | + #if DEBUG |
| 39 | + private let file: StaticString |
| 40 | + private let line: UInt |
| 41 | + private var prefix: String? |
| 42 | + private var previousState: (State) -> State? |
| 43 | + #endif |
| 44 | + @ObservedObject private var viewStore: ViewStore<State, Action> |
| 45 | + |
| 46 | + fileprivate init( |
| 47 | + store: Store<State, Action>, |
| 48 | + removeDuplicates isDuplicate: @escaping (State, State) -> Bool, |
| 49 | + file: StaticString = #fileID, |
| 50 | + line: UInt = #line, |
| 51 | + content: @escaping (ViewStore<State, Action>) -> Content |
| 52 | + ) { |
| 53 | + self.content = content |
| 54 | + #if DEBUG |
| 55 | + self.file = file |
| 56 | + self.line = line |
| 57 | + var previousState: State? = nil |
| 58 | + self.previousState = { currentState in |
| 59 | + defer { previousState = currentState } |
| 60 | + return previousState |
| 61 | + } |
| 62 | + #endif |
| 63 | + self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) |
50 | 64 | }
|
51 | 65 |
|
52 |
| - @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) |
53 |
| - extension WithViewStore: View where Content: View { |
54 |
| - /// Initializes a structure that transforms a store into an observable view store in order to |
55 |
| - /// compute views from store state. |
56 |
| - |
57 |
| - /// - Parameters: |
58 |
| - /// - store: A store. |
59 |
| - /// - isDuplicate: A function to determine when two `State` values are equal. When values are |
60 |
| - /// equal, repeat view computations are removed, |
61 |
| - /// - content: A function that can generate content from a view store. |
62 |
| - public init( |
63 |
| - _ store: Store<State, Action>, |
64 |
| - removeDuplicates isDuplicate: @escaping (State, State) -> Bool, |
65 |
| - @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
66 |
| - ) { |
67 |
| - self.content = content |
68 |
| - self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) |
69 |
| - } |
| 66 | + /// Prints debug information to the console whenever the view is computed. |
| 67 | + /// |
| 68 | + /// - Parameter prefix: A string with which to prefix all debug messages. |
| 69 | + /// - Returns: A structure that prints debug messages for all computations. |
| 70 | + public func debug(_ prefix: String = "") -> Self { |
| 71 | + var view = self |
| 72 | + #if DEBUG |
| 73 | + view.prefix = prefix |
| 74 | + #endif |
| 75 | + return view |
| 76 | + } |
70 | 77 |
|
71 |
| - public var body: Content { |
72 |
| - #if DEBUG |
73 |
| - if let prefix = self.prefix { |
74 |
| - print( |
75 |
| - """ |
76 |
| - \(prefix.isEmpty ? "" : "\(prefix): ")\ |
77 |
| - Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body |
78 |
| - """ |
79 |
| - ) |
| 78 | + fileprivate var _body: Content { |
| 79 | + #if DEBUG |
| 80 | + if let prefix = self.prefix { |
| 81 | + let difference = self.previousState(self.viewStore.state) |
| 82 | + .map { |
| 83 | + debugDiff($0, self.viewStore.state).map { "(Changed state)\n\($0)" } |
| 84 | + ?? "(No difference in state detected)" |
| 85 | +} |
| 86 | + ?? "(Initial state)\n\(debugOutput(self.viewStore.state, indent: 2))" |
| 87 | + func typeName(_ type: Any.Type) -> String { |
| 88 | + var name = String(reflecting: type) |
| 89 | + if let index = name.firstIndex(of: ".") { |
| 90 | + name.removeSubrange(...index) |
| 91 | + } |
| 92 | + return name |
80 | 93 | }
|
81 |
| - #endif |
82 |
| - return self.content(self.viewStore) |
83 |
| - } |
| 94 | + print( |
| 95 | + """ |
| 96 | + \(prefix.isEmpty ? "" : "\(prefix): ")\ |
| 97 | + WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\ |
| 98 | + @\(self.file):\(self.line) \(difference) |
| 99 | + """ |
| 100 | + ) |
| 101 | + } |
| 102 | + #endif |
| 103 | + return self.content(self.viewStore) |
84 | 104 | }
|
| 105 | +} |
85 | 106 |
|
86 | 107 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
87 |
| - extension WithViewStore where Content: View, State: Equatable { |
88 |
| - /// Initializes a structure that transforms a store into an observable view store in order to |
89 |
| - /// compute views from equatable store state. |
90 |
| - /// |
91 |
| - /// - Parameters: |
92 |
| - /// - store: A store of equatable state. |
93 |
| - /// - content: A function that can generate content from a view store. |
94 |
| - public init( |
95 |
| - _ store: Store<State, Action>, |
96 |
| - @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
97 |
| - ) { |
98 |
| - self.init(store, removeDuplicates: ==, content: content) |
99 |
| - } |
| 108 | +extension WithViewStore: View where Content: View { |
| 109 | + /// Initializes a structure that transforms a store into an observable view store in order to |
| 110 | + /// compute views from store state. |
| 111 | + /// |
| 112 | + /// - Parameters: |
| 113 | + /// - store: A store. |
| 114 | + /// - isDuplicate: A function to determine when two `State` values are equal. When values are |
| 115 | + /// equal, repeat view computations are removed, |
| 116 | + /// - content: A function that can generate content from a view store. |
| 117 | + public init( |
| 118 | + _ store: Store<State, Action>, |
| 119 | + removeDuplicates isDuplicate: @escaping (State, State) -> Bool, |
| 120 | + file: StaticString = #fileID, |
| 121 | + line: UInt = #line, |
| 122 | + @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
| 123 | + ) { |
| 124 | + self.init( |
| 125 | + store: store, |
| 126 | + removeDuplicates: isDuplicate, |
| 127 | + file: file, |
| 128 | + line: line, |
| 129 | + content: content |
| 130 | + ) |
100 | 131 | }
|
101 | 132 |
|
102 |
| - @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) |
103 |
| - extension WithViewStore where Content: View, State == Void { |
104 |
| - /// Initializes a structure that transforms a store into an observable view store in order to |
105 |
| - /// compute views from equatable store state. |
106 |
| - /// |
107 |
| - /// - Parameters: |
108 |
| - /// - store: A store of equatable state. |
109 |
| - /// - content: A function that can generate content from a view store. |
110 |
| - public init( |
111 |
| - _ store: Store<State, Action>, |
112 |
| - @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
113 |
| - ) { |
114 |
| - self.init(store, removeDuplicates: ==, content: content) |
115 |
| - } |
| 133 | + public var body: Content { |
| 134 | + self._body |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) |
| 139 | +extension WithViewStore where State: Equatable, Content: View { |
| 140 | + /// Initializes a structure that transforms a store into an observable view store in order to |
| 141 | + /// compute views from equatable store state. |
| 142 | + /// |
| 143 | + /// - Parameters: |
| 144 | + /// - store: A store of equatable state. |
| 145 | + /// - content: A function that can generate content from a view store. |
| 146 | + public init( |
| 147 | + _ store: Store<State, Action>, |
| 148 | + file: StaticString = #fileID, |
| 149 | + line: UInt = #line, |
| 150 | + @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
| 151 | + ) { |
| 152 | + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) |
116 | 153 | }
|
| 154 | +} |
| 155 | + |
| 156 | +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) |
| 157 | +extension WithViewStore where State == Void, Content: View { |
| 158 | + /// Initializes a structure that transforms a store into an observable view store in order to |
| 159 | + /// compute views from equatable store state. |
| 160 | + /// |
| 161 | + /// - Parameters: |
| 162 | + /// - store: A store of equatable state. |
| 163 | + /// - content: A function that can generate content from a view store. |
| 164 | + public init( |
| 165 | + _ store: Store<State, Action>, |
| 166 | + file: StaticString = #fileID, |
| 167 | + line: UInt = #line, |
| 168 | + @ViewBuilder content: @escaping (ViewStore<State, Action>) -> Content |
| 169 | + ) { |
| 170 | + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) |
| 171 | + } |
| 172 | +} |
117 | 173 |
|
118 | 174 | @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
|
119 |
| - extension WithViewStore: DynamicViewContent where State: Collection, Content: DynamicViewContent { |
120 |
| - public typealias Data = State |
| 175 | +extension WithViewStore: DynamicViewContent where State: Collection, Content: DynamicViewContent { |
| 176 | + public typealias Data = State |
121 | 177 |
|
122 |
| - public var data: State { |
123 |
| - self.viewStore.state |
124 |
| - } |
| 178 | + public var data: State { |
| 179 | + self.viewStore.state |
125 | 180 | }
|
| 181 | +} |
126 | 182 | #endif
|
127 | 183 |
|
128 | 184 | #if canImport(Combine) && canImport(SwiftUI) && compiler(>=5.3)
|
129 | 185 | import SwiftUI
|
130 | 186 |
|
131 |
| - /// A structure that transforms a store into an observable view store in order to compute scenes |
132 |
| - /// from store state. |
133 | 187 | @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
|
134 | 188 | extension WithViewStore: Scene where Content: Scene {
|
135 | 189 | /// Initializes a structure that transforms a store into an observable view store in order to
|
136 |
| - /// compute scenes from store state. |
137 |
| - |
| 190 | + /// compute views from store state. |
| 191 | + /// |
138 | 192 | /// - Parameters:
|
139 | 193 | /// - store: A store.
|
140 | 194 | /// - isDuplicate: A function to determine when two `State` values are equal. When values are
|
|
143 | 197 | public init(
|
144 | 198 | _ store: Store<State, Action>,
|
145 | 199 | removeDuplicates isDuplicate: @escaping (State, State) -> Bool,
|
| 200 | + file: StaticString = #fileID, |
| 201 | + line: UInt = #line, |
146 | 202 | @SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
|
147 | 203 | ) {
|
148 |
| - self.content = content |
149 |
| - self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) |
| 204 | + self.init( |
| 205 | + store: store, |
| 206 | + removeDuplicates: isDuplicate, |
| 207 | + file: file, |
| 208 | + line: line, |
| 209 | + content: content |
| 210 | + ) |
150 | 211 | }
|
151 | 212 |
|
152 | 213 | public var body: Content {
|
153 |
| - #if DEBUG |
154 |
| - if let prefix = self.prefix { |
155 |
| - print( |
156 |
| - """ |
157 |
| - \(prefix.isEmpty ? "" : "\(prefix): ")\ |
158 |
| - Evaluating WithViewStore<\(State.self), \(Action.self), ...>.body |
159 |
| - """ |
160 |
| - ) |
161 |
| - } |
162 |
| - #endif |
163 |
| - return self.content(self.viewStore) |
| 214 | + self._body |
164 | 215 | }
|
165 | 216 | }
|
166 | 217 |
|
167 | 218 | @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
|
168 |
| - extension WithViewStore where Content: Scene, State: Equatable { |
| 219 | +extension WithViewStore where State: Equatable, Content: Scene { |
169 | 220 | /// Initializes a structure that transforms a store into an observable view store in order to
|
170 |
| - /// compute views from equatable store state. |
| 221 | + /// compute scenes from equatable store state. |
171 | 222 | ///
|
172 | 223 | /// - Parameters:
|
173 | 224 | /// - store: A store of equatable state.
|
174 | 225 | /// - content: A function that can generate content from a view store.
|
175 | 226 | public init(
|
176 | 227 | _ store: Store<State, Action>,
|
| 228 | + file: StaticString = #fileID, |
| 229 | + line: UInt = #line, |
177 | 230 | @SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
|
178 | 231 | ) {
|
179 |
| - self.init(store, removeDuplicates: ==, content: content) |
| 232 | + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) |
180 | 233 | }
|
181 | 234 | }
|
182 | 235 |
|
183 | 236 | @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *)
|
184 |
| - extension WithViewStore where Content: Scene, State == Void { |
| 237 | +extension WithViewStore where State == Void, Content: Scene { |
185 | 238 | /// Initializes a structure that transforms a store into an observable view store in order to
|
186 |
| - /// compute views from equatable store state. |
| 239 | + /// compute scenes from equatable store state. |
187 | 240 | ///
|
188 | 241 | /// - Parameters:
|
189 | 242 | /// - store: A store of equatable state.
|
190 | 243 | /// - content: A function that can generate content from a view store.
|
191 | 244 | public init(
|
192 | 245 | _ store: Store<State, Action>,
|
| 246 | + file: StaticString = #fileID, |
| 247 | + line: UInt = #line, |
193 | 248 | @SceneBuilder content: @escaping (ViewStore<State, Action>) -> Content
|
194 | 249 | ) {
|
195 |
| - self.init(store, removeDuplicates: ==, content: content) |
| 250 | + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) |
196 | 251 | }
|
197 | 252 | }
|
198 | 253 |
|
|
0 commit comments