From 8e994412e3f204af88744d2d10f22f14cb87516c Mon Sep 17 00:00:00 2001 From: Alex Odawa Date: Wed, 17 Jun 2026 10:31:18 +0200 Subject: [PATCH 1/2] fix(a11y): drop assertionFailure for multiple Receivers in deferral container The receivers.count <= 1 guard already recovers safely by clearing receiver content and returning early. The assertionFailure traps under -Onone (e.g. snapshot tests), crashing instead of degrading gracefully. Co-Authored-By: Claude Opus 4.8 (1M context) --- BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift b/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift index faa52870c..4edc67e9d 100644 --- a/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift +++ b/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift @@ -227,7 +227,6 @@ extension AccessibilityDeferral { guard receivers.count <= 1 else { - assertionFailure("AccessibilityDeferral.ParentContainer must contain at most one Receiver; found \(receivers.count).") receivers.forEach { $0.apply(content: nil, frameProvider: nil) } return } From cb05c542c57b2fc4eb0e684b8ab3312a5150dc47 Mon Sep 17 00:00:00 2001 From: Alex Odawa Date: Wed, 17 Jun 2026 10:36:53 +0200 Subject: [PATCH 2/2] docs(a11y): explain tolerated multi-Receiver case + add CHANGELOG entry Add a comment on the receivers.count <= 1 guard documenting that >1 is an expected transient state during reentrant Blueprint updates (so the assertion isn't re-added), and record the assertion removal in CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Sources/AccessibilityDeferral.swift | 4 ++++ CHANGELOG.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift b/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift index 4edc67e9d..7ddb06d40 100644 --- a/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift +++ b/BlueprintUIAccessibilityCore/Sources/AccessibilityDeferral.swift @@ -226,6 +226,10 @@ extension AccessibilityDeferral { sources.forEach { $0.accessibilityElementsHidden = false } + // During a reentrant Blueprint update (e.g. a forced layoutBelowIfNeeded mid-update), + // this container can transiently observe both the outgoing and incoming Receiver before + // the hierarchy settles. That isn't a misconfiguration, so we recover and bail; the + // settled layout pass re-runs updateAccessibility() with the single surviving Receiver. guard receivers.count <= 1 else { receivers.forEach { $0.apply(content: nil, frameProvider: nil) } return diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a74fbcb..5b4c3c21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `AccessibilityDeferral` no longer aborts debug builds when its container transiently observes more than one `Receiver`. The `assertionFailure` treated more than one `Receiver` as a misconfiguration, but that state became reachable once `BlueprintView` began deferring reentrant updates (#618) instead of crashing on them: a reentrant layout pass can now run while the hierarchy is mid-swap, with the outgoing and incoming `Receiver` both briefly present. Previously that reentrancy tripped `BlueprintView`'s precondition first, so the deferral container never reached this state. The assertion was removed in favor of the existing safe recovery (clearing receiver content and bailing); the settled layout pass re-applies content with the single surviving `Receiver`. + ### Added ### Removed