Refactor: Extract shared hydration context setup into StateRegistration.withHydration

- Add StateRegistration.withHydration(context:_:) helper
- Replace duplicated save/set/restore pattern in renderToBuffer and measureChild
- Single source of truth for hydration context management
This commit is contained in:
phranck
2026-02-16 01:32:03 +01:00
parent 5058d9e724
commit 0feb9e9ee9
3 changed files with 43 additions and 44 deletions
+3 -22
View File
@@ -187,29 +187,10 @@ public func measureChild<V: View>(_ view: V, proposal: ProposedSize, context: Re
// Skip Renderable views: their rendering logic (including environment
// injection) lives in renderToBuffer, not in body. They fall through
// to the render-to-measure fallback below, which runs the full pipeline.
//
// Must set up hydration context before evaluating body, just like
// renderToBuffer does. Otherwise @Environment(T.self) lookups and
// @State self-hydration crash because activeEnvironment/activeContext
// are nil.
if !(view is Renderable), V.Body.self != Never.self {
let previousContext = StateRegistration.activeContext
let previousCounter = StateRegistration.counter
let previousEnvironment = StateRegistration.activeEnvironment
StateRegistration.activeContext = HydrationContext(
identity: context.identity,
storage: context.environment.stateStorage!
)
StateRegistration.counter = 0
StateRegistration.activeEnvironment = context.environment
let body = view.body
StateRegistration.activeContext = previousContext
StateRegistration.counter = previousCounter
StateRegistration.activeEnvironment = previousEnvironment
let body = StateRegistration.withHydration(context: context) {
view.body
}
return measureChild(body, proposal: proposal, context: context)
}
+6 -22
View File
@@ -177,32 +177,16 @@ public func renderToBuffer<V: View>(_ view: V, context: RenderContext) -> FrameB
if V.Body.self != Never.self {
let childContext = context.withChildIdentity(type: V.Body.self)
// Save previous hydration state (supports nested composite views).
let previousContext = StateRegistration.activeContext
let previousCounter = StateRegistration.counter
let previousEnvironment = StateRegistration.activeEnvironment
// Activate hydration: @State.init will use this to look up persistent storage.
// Activate environment: @Environment reads from activeEnvironment during body.
StateRegistration.activeContext = HydrationContext(
identity: context.identity,
storage: context.environment.stateStorage!
)
StateRegistration.counter = 0
StateRegistration.activeEnvironment = context.environment
// Wrap body evaluation in observation tracking so that any @Observable
// property accessed during body triggers a re-render when mutated.
let body = withObservationTracking {
view.body
} onChange: {
AppState.shared.setNeedsRenderWithCacheClear()
let body = StateRegistration.withHydration(context: context) {
withObservationTracking {
view.body
} onChange: {
AppState.shared.setNeedsRenderWithCacheClear()
}
}
// Restore previous hydration state and mark this identity as active for GC.
StateRegistration.activeContext = previousContext
StateRegistration.counter = previousCounter
StateRegistration.activeEnvironment = previousEnvironment
context.environment.stateStorage!.markActive(context.identity)
return renderToBuffer(body, context: childContext)
+34
View File
@@ -176,6 +176,40 @@ public enum StateRegistration {
/// Used by `@Environment` to read environment values during `body` evaluation.
/// Set alongside ``activeContext`` in `renderToBuffer(_:context:)`.
nonisolated(unsafe) public static var activeEnvironment: EnvironmentValues?
/// Evaluates a closure with a hydration context active.
///
/// Sets up `activeContext`, `counter`, and `activeEnvironment` before
/// calling the closure, then restores the previous state. This pattern
/// is needed whenever `view.body` is evaluated outside the normal
/// `renderToBuffer` dispatch (e.g., in `measureChild`).
///
/// - Parameters:
/// - context: The render context providing identity and environment.
/// - block: The closure to execute with hydration active.
/// - Returns: The result of the closure.
public static func withHydration<R>(
context: RenderContext,
_ block: () -> R
) -> R {
let previousContext = activeContext
let previousCounter = counter
let previousEnvironment = activeEnvironment
activeContext = HydrationContext(
identity: context.identity,
storage: context.environment.stateStorage!
)
counter = 0
activeEnvironment = context.environment
let result = block()
activeContext = previousContext
counter = previousCounter
activeEnvironment = previousEnvironment
return result
}
}
// MARK: - Binding