Expose Perception check suppression as SPI - #397
johnnewman-square wants to merge 1 commit into
Conversation
Suppression of Perception's debug-only runtime check is applied at two places in this module: Store state reads, and the workflow render pass that workflowPreview drives from its representable callbacks. Both are covered, but the funnel deciding when suppression applies is internal, so a preview host outside this module cannot reach it. MarketWorkflowUI is such a host. It declares its own PreviewView rather than building on workflowPreview, drives a render pass the same way, and so reproduces the same misreported reads for any workflow previewed through marketPreview. With no entry point here it would have to duplicate the predicate, which means a second copy of the rule in another repository and a new direct dependency on swift-perception for a module that has no other use for one. Expose the existing funnel under the PreviewHosting SPI instead. Its signature names no Perception type, so a host can adopt it with an @_spi import and nothing else. Behavior is unchanged: same availability gate, same predicate, same call sites within this module. Document the pre-iOS 17 limitation while that reasoning is in one place. Suppression stays off below iOS 17 because a view body there genuinely needs WithPerceptionTracking to observe state at all, so a warning about one is actionable and worth keeping. 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.
|
🤖 Closing this as unnecessary — the premise doesn't hold for the consumer that motivated it. I couldn't reproduce the render-pass warning in a downstream preview host at all, even with suppression removed entirely. The reason is that the check is compiled out of any module whose deployment floor is iOS 17 or later. Not disabled at runtime — eliminated by overload resolution. Both
Isolated repro of the language rule — same two-overload shape, one file, two deployment targets: protocol Perceptible {}
protocol Observableish {}
struct Registrar {
@available(*, deprecated, message: "PERCEPTIBLE OVERLOAD SELECTED")
@_disfavoredOverload
func access<S: Perceptible, M>(_ s: S, keyPath: KeyPath<S, M>) {}
}
@available(macOS 14, *)
extension Registrar {
func access<S: Observableish, M>(_ s: S, keyPath: KeyPath<S, M>) {}
}
struct Model: Perceptible, Observableish {
var foo = 0
func go() { Registrar().access(self, keyPath: \.foo) }
}So a preview host at a 17+ floor has nothing to suppress: its own state reads cannot reach Worth being clear that this doesn't undercut #396 — it just scopes it. The one case left uncovered is a preview host that is itself below iOS 17. If one turns up, this is the right shape for it and the branch will still be here. |
Follow-up to #396, which suppresses Perception's runtime check in two places:
Storestate reads, and the workflow render pass thatworkflowPreviewdrives from itsUIViewControllerRepresentablecallbacks. Both are covered, but the funnel deciding when suppression applies is internal, so a preview host outside this module can't reach it.Downstream preview hosts exist. A host that declares its own
UIViewControllerRepresentablerather than building onworkflowPreviewdrives a render pass the same way, and so reproduces the same misreported reads for the workflows it previews. With no entry point here it would have to duplicate the predicate — a second copy of the rule in another repository, plus a new direct dependency onswift-perceptionfor a module that has no other use for one.Summary
PreviewHostingSPIWhy SPI rather than public
Preview hosts are few and all of them are library code, so this is meaningless to an app and doesn't belong in the public surface. The signature names no Perception type —
<T>(_ operation: () -> T) -> T— so a host adopts it with an@_spi(PreviewHosting) import WorkflowSwiftUI, requiring no Perception import and no new package.The alternative shapes were worse. Making it public puts a niche primitive in front of every app developer. Leaving it internal pushes each host to reimplement the predicate, which is the drift #396 just removed from this module, recreated across repository boundaries where nobody will notice it diverging.
On the pre-iOS 17 limitation
Worth stating explicitly rather than leaving as an implementation detail, since it isn't obvious from the code and this module supports back to iOS 16.
Suppression stays off below iOS 17 and its siblings. A view body on those versions genuinely does need
WithPerceptionTrackingto observe state at all, so 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:
WithPerceptionTrackingis a view modifier, and a workflow'srendercannot be wrapped in one. Splitting the funnel in two would fix that, at the price of two near-identical entry points whose distinction only matters pre-17. Not worth it — but the gap should be written down.Test plan
swift build --target WorkflowSwiftUI, debug and release — the release build matters here, since the function ispublicwith a#if DEBUGbodyswiftformat --lintclean acrossWorkflowSwiftUI/SourcesXcodePreviewsTestsand thetest_perceptionRuntimeWarnings*tests are untouched, and the change is a visibility modifier plus documentation — leaving these to CI rather than a localtuist testrunChecklist