|
| 1 | +// |
| 2 | +// Preferences.swift |
| 3 | +// AndroidSwiftUICore |
| 4 | +// |
| 5 | +// Child-to-parent data flow. Unlike `GeometryReader`, none of this crosses the |
| 6 | +// bridge: a preference is set and reduced entirely within one Swift resolve |
| 7 | +// pass, so the interpreter never learns preferences exist. `onPreferenceChange` |
| 8 | +// installs a collector for its subtree, resolves it, and reads the reduction. |
| 9 | +// |
| 10 | + |
| 11 | +/// A value a view publishes to its ancestors, combined across the subtree by |
| 12 | +/// `reduce`. |
| 13 | +public protocol PreferenceKey { |
| 14 | + associatedtype Value |
| 15 | + static var defaultValue: Value { get } |
| 16 | + static func reduce(value: inout Value, nextValue: () -> Value) |
| 17 | +} |
| 18 | + |
| 19 | +/// Accumulates the preferences declared in one subtree, keyed by type. |
| 20 | +public final class PreferenceCollector { |
| 21 | + |
| 22 | + private var values: [ObjectIdentifier: Any] = [:] |
| 23 | + /// One closure per key that folds this collector's value into another. |
| 24 | + /// Values are type-erased, so re-reducing them elsewhere needs the concrete |
| 25 | + /// key type captured here at record time. |
| 26 | + private var mergers: [ObjectIdentifier: (PreferenceCollector) -> Void] = [:] |
| 27 | + |
| 28 | + public init() {} |
| 29 | + |
| 30 | + /// Folds one published value in, starting from the key's default so the |
| 31 | + /// reduction is the same whether or not anything published before. |
| 32 | + internal func record<K: PreferenceKey>(_ key: K.Type, _ value: K.Value) { |
| 33 | + let id = ObjectIdentifier(key) |
| 34 | + var current = (values[id] as? K.Value) ?? K.defaultValue |
| 35 | + K.reduce(value: ¤t, nextValue: { value }) |
| 36 | + values[id] = current |
| 37 | + let reduced = current |
| 38 | + mergers[id] = { parent in parent.record(key, reduced) } |
| 39 | + } |
| 40 | + |
| 41 | + internal func value<K: PreferenceKey>(for key: K.Type) -> K.Value { |
| 42 | + (values[ObjectIdentifier(key)] as? K.Value) ?? K.defaultValue |
| 43 | + } |
| 44 | + |
| 45 | + /// Folds everything collected here into `other`. |
| 46 | + /// |
| 47 | + /// An observer scopes a collector to its subtree, but it must not swallow |
| 48 | + /// the keys it isn't watching — an ancestor observing a *different* key |
| 49 | + /// still needs to see what that subtree published. |
| 50 | + internal func propagate(into other: PreferenceCollector) { |
| 51 | + for merge in mergers.values { merge(other) } |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +/// Remembers the last value delivered for one key so an unchanged reduction |
| 56 | +/// doesn't fire the callback again — without this the write the callback |
| 57 | +/// usually performs would re-evaluate forever. |
| 58 | +internal final class PreferenceMemo<Value: Equatable> { |
| 59 | + private var last: Value? |
| 60 | + func shouldDeliver(_ value: Value) -> Bool { |
| 61 | + guard last != value else { return false } |
| 62 | + last = value |
| 63 | + return true |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +// MARK: - preference |
| 68 | + |
| 69 | +public struct _PreferenceView<Content: View, K: PreferenceKey>: View { |
| 70 | + internal let key: K.Type |
| 71 | + internal let value: K.Value |
| 72 | + internal let content: Content |
| 73 | + public typealias Body = Never |
| 74 | +} |
| 75 | + |
| 76 | +extension _PreferenceView: _ResolutionEffectView { |
| 77 | + public func _applyEffect(_ context: inout ResolveContext) -> any View { |
| 78 | + context.preferences?.record(key, value) |
| 79 | + return content |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +// MARK: - onPreferenceChange |
| 84 | + |
| 85 | +public struct _OnPreferenceChangeView<Content: View, K: PreferenceKey>: View where K.Value: Equatable { |
| 86 | + internal let key: K.Type |
| 87 | + internal let action: (K.Value) -> Void |
| 88 | + internal let content: Content |
| 89 | + public typealias Body = Never |
| 90 | +} |
| 91 | + |
| 92 | +extension _OnPreferenceChangeView: PrimitiveView { |
| 93 | + |
| 94 | + public func _render(in context: ResolveContext) -> RenderNode { |
| 95 | + // A fresh collector scopes the reduction to this subtree; resolving at |
| 96 | + // the same path keeps the wrapper identity-transparent. |
| 97 | + let collector = PreferenceCollector() |
| 98 | + var childContext = context |
| 99 | + childContext.preferences = collector |
| 100 | + let node = Evaluator.resolve(content, childContext) |
| 101 | + |
| 102 | + let value = collector.value(for: key) |
| 103 | + // Everything published here keeps flowing upward, not just the observed |
| 104 | + // key: chained observers each scope a collector, and dropping the rest |
| 105 | + // would strand an ancestor watching a different key. |
| 106 | + if let parent = context.preferences { |
| 107 | + collector.propagate(into: parent) |
| 108 | + } |
| 109 | + |
| 110 | + // Keyed by the preference type, not just the path: chained observers sit |
| 111 | + // at the SAME identity path, so a shared key would let each overwrite the |
| 112 | + // other's memo, make every delivery look new, and re-evaluate forever. |
| 113 | + let memoPath = context.path + ".preference." + String(describing: K.self) |
| 114 | + let memo = context.storage.persistentObject(at: memoPath) { |
| 115 | + PreferenceMemo<K.Value>() |
| 116 | + } |
| 117 | + if memo.shouldDeliver(value) { |
| 118 | + action(value) |
| 119 | + } |
| 120 | + return node |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +public extension View { |
| 125 | + |
| 126 | + /// Publishes a value to ancestors observing `key`. |
| 127 | + func preference<K: PreferenceKey>(key: K.Type, value: K.Value) -> _PreferenceView<Self, K> { |
| 128 | + _PreferenceView(key: key, value: value, content: self) |
| 129 | + } |
| 130 | + |
| 131 | + /// Observes the reduced value of `key` across this view's subtree. |
| 132 | + func onPreferenceChange<K: PreferenceKey>( |
| 133 | + _ key: K.Type, |
| 134 | + perform action: @escaping (K.Value) -> Void |
| 135 | + ) -> _OnPreferenceChangeView<Self, K> where K.Value: Equatable { |
| 136 | + _OnPreferenceChangeView(key: key, action: action, content: self) |
| 137 | + } |
| 138 | +} |
0 commit comments