diff --git a/WorkflowSwiftUI/Sources/PerceptionCheckSuppression.swift b/WorkflowSwiftUI/Sources/PerceptionCheckSuppression.swift index bcdc01a07..30bd0fbea 100644 --- a/WorkflowSwiftUI/Sources/PerceptionCheckSuppression.swift +++ b/WorkflowSwiftUI/Sources/PerceptionCheckSuppression.swift @@ -11,7 +11,28 @@ import Perception /// meant to be set once at app startup, and a preview has no equivalent entry point — the canvas /// instantiates a view directly, with no app delegate and no runtime to configure — so a preview /// would otherwise have no way to reach the configuration at all. -func withPerceptionCheckSuppressed(_ operation: () -> T) -> T { +/// +/// Two kinds of read need this. A ``Store`` read from a view body is the obvious one. The other is +/// a read a workflow makes of its own state during `render`, which never passes through a `Store`: +/// a preview host drives that render pass synchronously from a `UIViewControllerRepresentable` +/// callback, and *SwiftUI* is what calls that callback. Perception decides whether it is looking at +/// a SwiftUI view body by walking the call stack for AttributeGraph frames, so those frames are +/// present and the whole render pass is misreported. The same workflow in an app renders off a +/// runtime update instead, leaving no AttributeGraph frame on the stack, which is why these +/// warnings appear only in the canvas. +/// +/// Known limitation below iOS 17 and its siblings: the availability check leaves suppression off +/// there, because a view body on those versions genuinely does need `WithPerceptionTracking` to +/// observe state at all — a warning about one is actionable, and hiding it would turn a preview +/// that silently stops updating into a preview that silently stops updating for no visible reason. +/// The cost is that a render-pass read still warns on those versions, where nothing can act on it, +/// since `WithPerceptionTracking` is a view modifier and a workflow's `render` cannot be wrapped in +/// one. +/// +/// SPI rather than public API. It exists for preview hosts, of which there are few and all of them +/// library code, and it is meaningless to an app. +@_spi(PreviewHosting) +public func withPerceptionCheckSuppressed(_ operation: () -> T) -> T { #if DEBUG && canImport(Observation) if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *), Runtime.configuration.suppressPerceptionCheckingWhenUsingObservation diff --git a/WorkflowSwiftUI/Sources/Workflow+Preview.swift b/WorkflowSwiftUI/Sources/Workflow+Preview.swift index 091ed5d9c..9abefeafa 100644 --- a/WorkflowSwiftUI/Sources/Workflow+Preview.swift +++ b/WorkflowSwiftUI/Sources/Workflow+Preview.swift @@ -42,16 +42,9 @@ private struct PreviewView: UIViewControllerRepresentabl let customizeEnvironment: (inout ViewEnvironment) -> Void let onOutput: (WorkflowType.Output) -> Void - // Both representable callbacks drive a workflow render pass synchronously, and the check - // Perception performs there decides whether it is looking at a SwiftUI view body by walking the - // call stack for AttributeGraph frames. SwiftUI is what calls these methods, so those frames are - // present and every observable read the render pass makes is misreported — including reads a - // workflow makes of its own state, which never pass through a `Store` and so cannot be covered - // by the suppression there. - // - // These warnings are unique to previews. The same workflow running in an app renders off a - // runtime update rather than a SwiftUI one, so no AttributeGraph frame is on the stack and the - // check correctly stays quiet. + // Both callbacks drive a workflow render pass synchronously, and SwiftUI is what calls them, so + // every observable read the pass makes is misreported. See `withPerceptionCheckSuppressed` for + // why, and for why it only happens in previews. func makeUIViewController(context: Context) -> UIViewControllerType { withPerceptionCheckSuppressed {