Adds SwiftUI-style @Environment property wrapper with key-path and object-injection flavors, an @Observable bridge that flushes on a GLib idle source at highIdle priority (ahead of GTK redraw), targeted widget updates driven by Observation tracking, and two-way observable bindings that ride the existing bindProperty path. New library files (4): - State/ObservationBridge.swift -- coalesced flush bridge - State/EnvironmentValues.swift -- scope-local key-to-box map - State/Environment.swift -- @Environment wrapper, DynamicProperty - Core/View+Environment.swift -- .environment(...) modifiers Modified library files (8): - DependencyTracker: import Observation, observing: param, didArmObservation - EitherView/ForEach: opt container trackers out of Observation - NodeRegistry: isEmpty for retention check - PropertyBinding: hasDependencies || didArmObservation gate - Binding: readOnly init, observable key-path init - MountContext: environment field, withEnvironment, inheritance - AnyView: _resolveDynamicProperties hook before user body eval New tests (21 across 3 files): - EnvironmentTests (7): scoping, nesting, projections, ForEach, lazy branches, reuse - ObservableTests (11): acceptance case, write-back, coalescing, teardown, binding echo, re-arm de-dupe, retention, flush ordering - StateTests (+3): Observation arming unit tests Example: EnvironmentDemoPage demonstrates the feature end to end.
46 lines
2 KiB
Swift
46 lines
2 KiB
Swift
/// Context threaded through the mount pipeline. Carries the ``NodeRegistry``
|
|
/// that collects reactive resources for teardown, and the ``EnvironmentValues``
|
|
/// visible to the subtree being mounted.
|
|
@_spi(Portico) @MainActor public final class MountContext {
|
|
/// The registry for the current subtree; reactive sites register here.
|
|
@_spi(Portico) public let registry: NodeRegistry
|
|
|
|
/// The environment visible to this subtree.
|
|
///
|
|
/// Containers pass their context to children unchanged, so a context
|
|
/// produced by ``withEnvironment(_:)`` is seen by exactly the subtree the
|
|
/// `.environment(...)` modifier wraps - that is what makes injection
|
|
/// subtree-scoped without any extra plumbing in containers.
|
|
@_spi(Portico) public let environment: EnvironmentValues
|
|
|
|
/// Creates a root context with a fresh registry and an empty environment.
|
|
@_spi(Portico) public init() {
|
|
self.registry = NodeRegistry()
|
|
self.environment = EnvironmentValues()
|
|
}
|
|
|
|
private init(registry: NodeRegistry, environment: EnvironmentValues) {
|
|
self.registry = registry
|
|
self.environment = environment
|
|
}
|
|
|
|
/// Returns a child context whose registry is an independently-torn-down
|
|
/// child of this context's registry. Used per `ForEach` row so removing a
|
|
/// row releases only that row's resources. The environment is inherited.
|
|
@_spi(Portico) public func makeChild() -> MountContext {
|
|
let child = NodeRegistry()
|
|
registry.addChild(child)
|
|
return MountContext(registry: child, environment: environment)
|
|
}
|
|
|
|
/// Returns a context sharing this registry but carrying a derived
|
|
/// environment. Teardown scope is deliberately unchanged: injecting a value
|
|
/// does not create an independently-unmountable subtree.
|
|
@_spi(Portico) public func withEnvironment(
|
|
_ transform: (inout EnvironmentValues) -> Void
|
|
) -> MountContext {
|
|
var derived = environment
|
|
transform(&derived)
|
|
return MountContext(registry: registry, environment: derived)
|
|
}
|
|
}
|