Skip to content

fix: defer reentrant BlueprintView updates#618

Merged
robmaceachern merged 4 commits into
mainfrom
robmaceachern/defer-reentrant-safe-area-layout
Jun 16, 2026
Merged

fix: defer reentrant BlueprintView updates#618
robmaceachern merged 4 commits into
mainfrom
robmaceachern/defer-reentrant-safe-area-layout

Conversation

@robmaceachern

Copy link
Copy Markdown
Member

Summary

  • defer BlueprintView hierarchy invalidations that occur while Blueprint is already updating its view tree
  • coalesce the deferred retry so UIKit safe-area/window callbacks cannot synchronously re-enter updateViewHierarchyIfNeeded()
  • add a regression test that mutates additionalSafeAreaInsets during backing-view attach/layout, matching the production crash shape
  • add the missing BlueprintUICommonControls dependency for the package test target so the focused package test builds

Context

This came from an ios-register 7.10 crash investigation where BlueprintView.safeAreaInsetsDidChange() re-entered BlueprintView.updateViewHierarchyIfNeeded() during a Market keyboard/header layout update. A MarketUI harness reproduced the exact precondition crash locally before this Blueprint patch; with this patch copied into Market's Blueprint checkout, the same harness passes.

Testing

  • xcodebuild test -scheme BlueprintUI-Package -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.5' -only-testing:BlueprintUITests/BlueprintViewTests/test_safeAreaChangeDuringUpdateDefersNestedUpdate -quiet
  • Market validation with this Blueprint source copied into Market's local checkout: xcodebuild test -workspace ios/MarketDevelopment.xcworkspace -scheme UnitTests -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.5' -only-testing:MarketUI-Tests/MarketKeyboardReaderReentrantLayoutTests/test_safeAreaChangeDuringKeyboardReaderUpdateDoesNotReenterBlueprint -quiet

robmaceachern and others added 2 commits June 8, 2026 17:16
Add a regression test mirroring the navigation-bar crash shape from
production: a view callback replaces `element` and forces a synchronous
layout pass while the view hierarchy update is still in progress. The
test crashes on the reentrant-update precondition without the deferral
fix, and additionally asserts that the deferred update converges (the
replacement element's backing view is applied on a following main run
loop turn).

Also add the CHANGELOG entry, document why the deferred re-arm is
required (an immediate setNeedsLayout re-arm would prevent a forcing
layoutIfNeeded from terminating while the in-flight update still holds
isInsideUpdate), and update the lifecycle-callback comment that still
referenced the removed precondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@johnnewman-square johnnewman-square left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the best fix proposal so far. This should address all call sites, more than the isolated Market fix.

I'm in favor of trying this out until we find a tighter repro.

Comment on lines -389 to +431
precondition(
!isInsideUpdate,
"Reentrant updates are not supported in BlueprintView. Ensure that view events from within the hierarchy are not synchronously triggering additional updates."
)
if isInsideUpdate {
// A reentrant update was triggered from within the in-flight update, e.g. by a
// safe-area or keyboard change synchronously forcing a layout pass. Defer it so
// the current update can finish; the deferred pass picks up the latest state.
setNeedsViewHierarchyUpdate()
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging this case could be useful, if we'd like to track this going forward.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid infinite async layout churn, an agent suggested this:

I'd suggest a debug-only diagnostic — e.g., count consecutive deferred re-arms with no intervening quiet turn and assertionFailure/os_log past a threshold (say 10). That preserves the developer signal the precondition used to provide while keeping production safe.

I think there's value in logging and in debug asserts for cases like this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this look? bd980ae

@robmaceachern robmaceachern marked this pull request as ready for review June 16, 2026 16:29
@robmaceachern robmaceachern requested a review from a team as a code owner June 16, 2026 16:29

@johnnewman-square johnnewman-square left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new logging changes look great. Thanks for doing that.

@robmaceachern robmaceachern merged commit 67b5362 into main Jun 16, 2026
11 checks passed
@robmaceachern robmaceachern deleted the robmaceachern/defer-reentrant-safe-area-layout branch June 16, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants