14
14
* limitations under the License.
15
15
*/
16
16
17
+ import Foundation
17
18
import ViewEnvironment
18
19
19
20
/// A node in a `ViewEnvironment` propagation tree.
@@ -23,11 +24,13 @@ import ViewEnvironment
23
24
/// - Walking up the tree, via `environmentAncestor`.
24
25
/// - Walking down the tree, via `environmentDescendants`.
25
26
/// - Notifying a node that the environment changed, via `setNeedsEnvironmentUpdate()`.
26
- ///
27
- /// This framework provides conformance of this protocol to `UIViewController` and `UIView` via the
28
- /// `ViewEnvironmentPropagatingObject` protocol.
27
+ /// - Override the ancestor and descendants
28
+ /// - Add environment update observations
29
+ /// - Flag the backing object for needing to have the `ViewEnvironment` reapplied (e.g. `setNeedsLayout()`)
29
30
///
30
- public protocol ViewEnvironmentPropagating {
31
+ /// This framework provides conformance of this protocol to `UIViewController` and `UIView`.
32
+ ///
33
+ public protocol ViewEnvironmentPropagating : AnyObject {
31
34
/// Calling this will flag this node for needing to update the `ViewEnvironment`. For `UIView`/`UIViewController`,
32
35
/// this will occur on the next layout pass (`setNeedsLayout` will be called on the caller's behalf).
33
36
///
@@ -44,12 +47,12 @@ public protocol ViewEnvironmentPropagating {
44
47
/// This describes the ancestor that the `ViewEnvironment` is inherited from.
45
48
///
46
49
/// To override the return value of this property for `UIViewController`/`UIView` subclasses, set the
47
- /// `environmentAncestorOverride` property. If no override is present, the return value will be `parent ??
50
+ /// `environmentAncestorOverride` property. If no override is present, the return value will be `parent ??
48
51
/// `presentingViewController`/`superview`.
49
52
///
50
- /// Ancestor-descendent bindings must be mutually agreed. If the value of the ancestor is `nil`, then by default,
51
- /// other nodes configured with this node as a descendant will not notify this node of needing an environment
52
- /// update as it changes. This allows a node to effectively act as a root node when needed (e.g. bridging from
53
+ /// Ancestor-descendent bindings must be mutually agreed. If the value of the ancestor is `nil`, then by default,
54
+ /// other nodes configured with this node as a descendant will not notify this node of needing an environment
55
+ /// update as it changes. This allows a node to effectively act as a root node when needed (e.g. bridging from
53
56
/// other propagation systems like WorkflowUI).
54
57
///
55
58
@_spi ( ViewEnvironmentWiring)
@@ -58,17 +61,35 @@ public protocol ViewEnvironmentPropagating {
58
61
/// The `ViewEnvironment` propagation descendants.
59
62
///
60
63
/// This describes the descendants that will be notified when the `ViewEnvironment` changes.
61
- ///
62
- /// Ancestor-descendent bindings must be mutually agreed. If a descendant's `environmentAncestor` is `nil`, that
63
- /// descendant will not be notified when the `ViewEnvironment` changes.
64
+ ///
65
+ /// Ancestor-descendent bindings must be mutually agreed. If a descendant's `environmentAncestor` is not `self`,
66
+ /// that descendant will not be notified when the `ViewEnvironment` changes.
64
67
///
65
68
/// To override the return value of this property for `UIViewController`/`UIView` subclasses, set the
66
- /// `environmentDescendantsOverride` property. If no override is present, the return value will be a collection
67
- /// of all `children` in addition to the `presentedViewController` for `UIViewController`s and `subviews` for
69
+ /// `environmentDescendantsOverride` property. If no override is present, the return value will be a collection
70
+ /// of all `children` in addition to the `presentedViewController` for `UIViewController`s and `subviews` for
68
71
/// `UIView`s.
69
72
///
70
73
@_spi ( ViewEnvironmentWiring)
71
74
var environmentDescendants : [ ViewEnvironmentPropagating ] { get }
75
+
76
+ /// The default ancestor for `ViewEnvironment` propagation.
77
+ ///
78
+ @_spi ( ViewEnvironmentWiring)
79
+ var defaultEnvironmentAncestor : ViewEnvironmentPropagating ? { get }
80
+
81
+ /// The default descendants for `ViewEnvironment` propagation.
82
+ ///
83
+ @_spi ( ViewEnvironmentWiring)
84
+ var defaultEnvironmentDescendants : [ ViewEnvironmentPropagating ] { get }
85
+
86
+ /// Informs the backing object that this specific node should be flagged for another application of the
87
+ /// `ViewEnvironment`.
88
+ ///
89
+ /// For `UIViewController`/`UIView`s this typically corresponds to `setNeedsLayout()`.
90
+ ///
91
+ @_spi ( ViewEnvironmentWiring)
92
+ func setNeedsApplyEnvironment( )
72
93
}
73
94
74
95
extension ViewEnvironmentPropagating {
@@ -92,7 +113,7 @@ extension ViewEnvironmentPropagating {
92
113
/// for the system to call `apply(context:)` when appropriate (e.g. on the next layout pass for
93
114
/// `UIViewController`/`UIView` subclasses).
94
115
///
95
- /// - Important: `UIViewController` and `UIView` conformers _must_ call `applyEnvironmentIfNeeded()` in
116
+ /// - Important: `UIViewController` and `UIView` conformers _must_ call `applyEnvironmentIfNeeded()` in
96
117
/// `viewWillLayoutSubviews()` and `layoutSubviews()` respectively.
97
118
///
98
119
public var environment : ViewEnvironment {
@@ -105,22 +126,250 @@ extension ViewEnvironmentPropagating {
105
126
return environment
106
127
}
107
128
129
+ /// Consumers _must_ call this function when the environment should be re-applied, e.g. in
130
+ /// `viewWillLayoutSubviews()` for `UIViewController`s and `layoutSubviews()` for `UIView`s.
131
+ ///
132
+ /// This will call `apply(environment:)` on the receiver if the node has been flagged for needing update.
133
+ ///
134
+ public func applyEnvironmentIfNeeded( ) {
135
+ guard needsEnvironmentUpdate else { return }
136
+
137
+ needsEnvironmentUpdate = false
138
+
139
+ if let observing = self as? ViewEnvironmentObserving {
140
+ let environment = observing. environment
141
+ observing. apply ( environment: environment)
142
+ }
143
+ }
144
+
108
145
/// Notifies all appropriate descendants that the environment needs update.
109
146
///
110
- /// Ancestor-descendent bindings must be mutually agreed for this method to notify them. If a descendant's
111
- /// `environmentAncestor` is `nil ` it will not be notified of needing update.
147
+ /// Ancestor-descendent bindings must be mutually agreed for this method to notify them. If a descendant's
148
+ /// `environmentAncestor` is not `self ` it will not be notified of needing update.
112
149
///
113
150
@_spi ( ViewEnvironmentWiring)
114
151
public func setNeedsEnvironmentUpdateOnAppropriateDescendants( ) {
115
152
for descendant in environmentDescendants {
116
- // If the descendant's `environmentAncestor` is nil it has opted out of environment updates and is likely
117
- // acting as a root for propagation bridging purposes (e.g. from a Workflow ViewEnvironment update).
153
+ // If the descendant's `environmentAncestor` is not `self` it has opted out of environment updates from this
154
+ // node. The node is is likely acting as a root for propagation bridging purposes (e.g. from a Workflow
155
+ // ViewEnvironment update).
118
156
// Avoid updating the descendant if this is the case.
119
- guard descendant. environmentAncestor != nil else {
120
- continue
157
+ if descendant. environmentAncestor === self {
158
+ descendant . setNeedsEnvironmentUpdate ( )
121
159
}
160
+ }
161
+ }
122
162
123
- descendant. setNeedsEnvironmentUpdate ( )
163
+ /// Adds a `ViewEnvironment` change observation.
164
+ ///
165
+ /// The observation will only be active for as long as the returned lifetime is retained or
166
+ /// `cancel()` is called on it.
167
+ ///
168
+ @_spi ( ViewEnvironmentWiring)
169
+ public func addEnvironmentNeedsUpdateObserver(
170
+ _ onNeedsUpdate: @escaping ( ViewEnvironment ) -> Void
171
+ ) -> ViewEnvironmentUpdateObservationLifetime {
172
+ let object = ViewEnvironmentUpdateObservationKey ( )
173
+ needsUpdateObservers [ object] = onNeedsUpdate
174
+ return . init { [ weak self] in
175
+ self ? . needsUpdateObservers [ object] = nil
176
+ }
177
+ }
178
+
179
+ /// The `ViewEnvironment` propagation ancestor.
180
+ ///
181
+ /// This describes the ancestor that the `ViewEnvironment` is inherited from.
182
+ ///
183
+ /// To override the return value of this property, set the `environmentAncestorOverride`.
184
+ /// If no override is present, the return value will be `defaultEnvironmentAncestor`.
185
+ ///
186
+ @_spi ( ViewEnvironmentWiring)
187
+ public var environmentAncestor : ViewEnvironmentPropagating ? {
188
+ environmentAncestorOverride ? ( ) ?? defaultEnvironmentAncestor
189
+ }
190
+
191
+ /// The `ViewEnvironment` propagation descendants.
192
+ ///
193
+ /// This describes the descendants that will be notified when the `ViewEnvironment` changes.
194
+ ///
195
+ /// If a descendant's `environmentAncestor` is not `self`, that descendant will not be notified when the
196
+ /// `ViewEnvironment` changes.
197
+ ///
198
+ /// To override the return value of this property, set the `environmentDescendantsOverride`.
199
+ /// If no override is present, the return value will be `defaultEnvironmentDescendants`.
200
+ ///
201
+ @_spi ( ViewEnvironmentWiring)
202
+ public var environmentDescendants : [ ViewEnvironmentPropagating ] {
203
+ environmentDescendantsOverride ? ( ) ?? defaultEnvironmentDescendants
204
+ }
205
+
206
+ /// ## SeeAlso ##
207
+ /// - `environmentAncestorOverride`
208
+ ///
209
+ @_spi ( ViewEnvironmentWiring)
210
+ public typealias EnvironmentAncestorProvider = ( ) -> ViewEnvironmentPropagating ?
211
+
212
+ /// This property allows you to override the propagation path of the `ViewEnvironment` as it flows through the
213
+ /// node hierarchy by overriding the return value of `environmentAncestor`.
214
+ ///
215
+ /// The result of this closure should typically be the propagation node that the `ViewEnvironment` is inherited
216
+ /// from. If the value of the ancestor is nil, by default, other nodes configured with this node as a descendant
217
+ /// will not notify this node of needing an environment update as it changes. This allows a node to effectively
218
+ /// act as a root node when needed (e.g. bridging from other propagation systems like WorkflowUI).
219
+ ///
220
+ /// If this value is `nil` (the default), the resolved value for the ancestor will be `defaultEnvironmentAncestor`.
221
+ ///
222
+ /// ## Important ##
223
+ /// - You must not set overrides while overrides are already set—doing so will throw an assertion. This assertion
224
+ /// prevents accidentally clobbering an existing propagation path customization defined somewhere out of your
225
+ /// control (e.g. Modals customization).
226
+ ///
227
+ @_spi ( ViewEnvironmentWiring)
228
+ public var environmentAncestorOverride : EnvironmentAncestorProvider ? {
229
+ get {
230
+ objc_getAssociatedObject ( self , & AssociatedKeys. ancestorOverride) as? EnvironmentAncestorProvider
231
+ }
232
+ set {
233
+ assert (
234
+ newValue == nil
235
+ || environmentAncestorOverride == nil ,
236
+ " Attempting to set environment ancestor override when one is already set. "
237
+ )
238
+ objc_setAssociatedObject ( self , & AssociatedKeys. ancestorOverride, newValue, . OBJC_ASSOCIATION_RETAIN)
239
+ }
240
+ }
241
+
242
+ /// ## SeeAlso ##
243
+ /// - `environmentDescendantsOverride`
244
+ ///
245
+ @_spi ( ViewEnvironmentWiring)
246
+ public typealias EnvironmentDescendantsProvider = ( ) -> [ ViewEnvironmentPropagating ]
247
+
248
+ /// This property allows you to override the propagation path of the `ViewEnvironment` as it flows through the
249
+ /// node hierarchy by overriding the return value of `environmentDescendants`.
250
+ ///
251
+ /// The result of closure var should be the node that should be informed that there has been an update with the
252
+ /// `ViewEnvironment` updates.
253
+ ///
254
+ /// If this value is `nil` (the default), the `environmentDescendants` will be resolved to
255
+ /// `defaultEnvironmentDescendants`.
256
+ ///
257
+ /// ## Important ##
258
+ /// - You must not set overrides while overrides are already set. Doing so will throw an
259
+ /// assertion.
260
+ ///
261
+ @_spi ( ViewEnvironmentWiring)
262
+ public var environmentDescendantsOverride : EnvironmentDescendantsProvider ? {
263
+ get {
264
+ objc_getAssociatedObject ( self , & AssociatedKeys. descendantsOverride) as? EnvironmentDescendantsProvider
265
+ }
266
+ set {
267
+ assert (
268
+ newValue == nil
269
+ || environmentDescendantsOverride == nil ,
270
+ " Attempting to set environment descendants override when one is already set. "
271
+ )
272
+ objc_setAssociatedObject ( self , & AssociatedKeys. descendantsOverride, newValue, . OBJC_ASSOCIATION_RETAIN)
273
+ }
274
+ }
275
+ }
276
+
277
+ /// A closure that is called when the `ViewEnvironment` needs to be updated.
278
+ ///
279
+ public typealias ViewEnvironmentUpdateObservation = ( ViewEnvironment ) -> Void
280
+
281
+ /// Describes the lifetime of a `ViewEnvironment` update observation.
282
+ ///
283
+ /// The observation will be removed when `remove()` is called or the lifetime token is
284
+ /// de-initialized.
285
+ ///
286
+ /// ## SeeAlso ##
287
+ /// - `addEnvironmentNeedsUpdateObserver(_:)`
288
+ ///
289
+ public final class ViewEnvironmentUpdateObservationLifetime {
290
+ /// Removes the observation.
291
+ ///
292
+ /// This is called in `deinit`.
293
+ ///
294
+ public func remove( ) {
295
+ onRemove ( )
296
+ }
297
+
298
+ private let onRemove : ( ) -> Void
299
+
300
+ init ( onRemove: @escaping ( ) -> Void ) {
301
+ self . onRemove = onRemove
302
+ }
303
+
304
+ deinit {
305
+ remove ( )
306
+ }
307
+ }
308
+
309
+ private enum ViewEnvironmentPropagatingNSObjectAssociatedKeys {
310
+ static var needsEnvironmentUpdate = NSObject ( )
311
+ static var needsUpdateObservers = NSObject ( )
312
+ static var ancestorOverride = NSObject ( )
313
+ static var descendantsOverride = NSObject ( )
314
+ }
315
+
316
+ extension ViewEnvironmentPropagating {
317
+ private typealias AssociatedKeys = ViewEnvironmentPropagatingNSObjectAssociatedKeys
318
+
319
+ public func setNeedsEnvironmentUpdate( ) {
320
+ needsEnvironmentUpdate = true
321
+
322
+ if !needsUpdateObservers. isEmpty {
323
+ let environment = environment
324
+
325
+ for observer in needsUpdateObservers. values {
326
+ observer ( environment)
327
+ }
328
+ }
329
+
330
+ if let observer = self as? ViewEnvironmentObserving {
331
+ observer. environmentDidChange ( )
332
+
333
+ setNeedsApplyEnvironment ( )
334
+ }
335
+
336
+ setNeedsEnvironmentUpdateOnAppropriateDescendants ( )
337
+ }
338
+
339
+ private var needsUpdateObservers : [ ViewEnvironmentUpdateObservationKey : ViewEnvironmentUpdateObservation ] {
340
+ get {
341
+ objc_getAssociatedObject (
342
+ self ,
343
+ & AssociatedKeys. needsUpdateObservers
344
+ ) as? [ ViewEnvironmentUpdateObservationKey : ViewEnvironmentUpdateObservation ] ?? [ : ]
345
+ }
346
+ set {
347
+ objc_setAssociatedObject (
348
+ self ,
349
+ & AssociatedKeys. needsUpdateObservers,
350
+ newValue,
351
+ . OBJC_ASSOCIATION_RETAIN_NONATOMIC
352
+ )
353
+ }
354
+ }
355
+
356
+ var needsEnvironmentUpdate : Bool {
357
+ get {
358
+ let associatedObject = objc_getAssociatedObject (
359
+ self ,
360
+ & AssociatedKeys. needsEnvironmentUpdate
361
+ )
362
+ return ( associatedObject as? Bool ) ?? true
363
+ }
364
+ set {
365
+ objc_setAssociatedObject (
366
+ self ,
367
+ & AssociatedKeys. needsEnvironmentUpdate,
368
+ newValue,
369
+ objc_AssociationPolicy. OBJC_ASSOCIATION_COPY
370
+ )
124
371
}
125
372
}
126
373
}
374
+
375
+ private class ViewEnvironmentUpdateObservationKey : NSObject { }
0 commit comments