Skip to content

Commit 13c21bd

Browse files
authored
[feat]: merge ViewEnvironmentPropagatingObject into ViewEnvironmentPropagating (#218)
1 parent f52fe53 commit 13c21bd

6 files changed

+281
-312
lines changed

ViewEnvironmentUI/Sources/UIView+ViewEnvironmentPropagatingObject.swift renamed to ViewEnvironmentUI/Sources/UIView+ViewEnvironmentPropagating.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import UIKit
2020
import ViewEnvironment
2121

22-
extension UIView: ViewEnvironmentPropagatingObject {
22+
extension UIView: ViewEnvironmentPropagating {
2323
@_spi(ViewEnvironmentWiring)
2424
public var defaultEnvironmentAncestor: ViewEnvironmentPropagating? { superview }
2525

ViewEnvironmentUI/Sources/UIViewController+ViewEnvironmentPropagatingObject.swift renamed to ViewEnvironmentUI/Sources/UIViewController+ViewEnvironmentPropagating.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import UIKit
2020
import ViewEnvironment
2121

22-
extension UIViewController: ViewEnvironmentPropagatingObject {
22+
extension UIViewController: ViewEnvironmentPropagating {
2323
@_spi(ViewEnvironmentWiring)
2424
public var defaultEnvironmentAncestor: ViewEnvironmentPropagating? { parent ?? presentingViewController }
2525

ViewEnvironmentUI/Sources/ViewEnvironmentPropagating.swift

+270-21
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import Foundation
1718
import ViewEnvironment
1819

1920
/// A node in a `ViewEnvironment` propagation tree.
@@ -23,11 +24,13 @@ import ViewEnvironment
2324
/// - Walking up the tree, via `environmentAncestor`.
2425
/// - Walking down the tree, via `environmentDescendants`.
2526
/// - 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()`)
2930
///
30-
public protocol ViewEnvironmentPropagating {
31+
/// This framework provides conformance of this protocol to `UIViewController` and `UIView`.
32+
///
33+
public protocol ViewEnvironmentPropagating: AnyObject {
3134
/// Calling this will flag this node for needing to update the `ViewEnvironment`. For `UIView`/`UIViewController`,
3235
/// this will occur on the next layout pass (`setNeedsLayout` will be called on the caller's behalf).
3336
///
@@ -44,12 +47,12 @@ public protocol ViewEnvironmentPropagating {
4447
/// This describes the ancestor that the `ViewEnvironment` is inherited from.
4548
///
4649
/// 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 ??
4851
/// `presentingViewController`/`superview`.
4952
///
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
5356
/// other propagation systems like WorkflowUI).
5457
///
5558
@_spi(ViewEnvironmentWiring)
@@ -58,17 +61,35 @@ public protocol ViewEnvironmentPropagating {
5861
/// The `ViewEnvironment` propagation descendants.
5962
///
6063
/// 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.
6467
///
6568
/// 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
6871
/// `UIView`s.
6972
///
7073
@_spi(ViewEnvironmentWiring)
7174
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()
7293
}
7394

7495
extension ViewEnvironmentPropagating {
@@ -92,7 +113,7 @@ extension ViewEnvironmentPropagating {
92113
/// for the system to call `apply(context:)` when appropriate (e.g. on the next layout pass for
93114
/// `UIViewController`/`UIView` subclasses).
94115
///
95-
/// - Important: `UIViewController` and `UIView` conformers _must_ call `applyEnvironmentIfNeeded()` in
116+
/// - Important: `UIViewController` and `UIView` conformers _must_ call `applyEnvironmentIfNeeded()` in
96117
/// `viewWillLayoutSubviews()` and `layoutSubviews()` respectively.
97118
///
98119
public var environment: ViewEnvironment {
@@ -105,22 +126,250 @@ extension ViewEnvironmentPropagating {
105126
return environment
106127
}
107128

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+
108145
/// Notifies all appropriate descendants that the environment needs update.
109146
///
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.
112149
///
113150
@_spi(ViewEnvironmentWiring)
114151
public func setNeedsEnvironmentUpdateOnAppropriateDescendants() {
115152
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).
118156
// 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()
121159
}
160+
}
161+
}
122162

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+
)
124371
}
125372
}
126373
}
374+
375+
private class ViewEnvironmentUpdateObservationKey: NSObject {}

0 commit comments

Comments
 (0)