Skip to content

Fix state-during-view-update faults in text selection#67

Open
adamtheturtle wants to merge 1 commit into
gonzalezreal:mainfrom
adamtheturtle:fix/selection-multiple-updates-per-frame
Open

Fix state-during-view-update faults in text selection#67
adamtheturtle wants to merge 1 commit into
gonzalezreal:mainfrom
adamtheturtle:fix/selection-multiple-updates-per-frame

Conversation

@adamtheturtle

@adamtheturtle adamtheturtle commented Jun 15, 2026

Copy link
Copy Markdown

Problem

Fixes #22.

With text selection enabled (.textual.textSelection(.enabled)), SwiftUI logs non-fatal faults on layout passes. The issue reports the iOS AnyTextLayoutCollection message; on macOS there's additionally an onChange(of: Layout) variant and — once the duplicate handler is removed — a Modifying state during view update fault. All stem from the selection layer writing observed/@State values during a view update.

Changes

AppKitTextSelectionView stored selection rectangles in @State and seeded them from two onChange(initial: true) handlers (one on selectedRange, one on layout), both writing the same state during the initial view update — two writes in one frame (multiple times per frame), and a single surviving write still during the update (Modifying state during view update). This derives the rectangles as an @Observable-tracked computed property instead:

private var selectionRects: [TextSelectionRect] {
  guard let textSelectionModel, let selectedRange = textSelectionModel.selectedRange else { return [] }
  return textSelectionModel.selectionRects(for: selectedRange, layout: layout)
}

No @State, no onChange, no state mutation during the update. It still recomputes whenever the selected range or layout changes (the read of selectedRange is tracked by Observation), so selection rendering is unchanged.

TextSelectionModel.setLayoutCollection re-wrote the observed selectedRange during reconciliation even when the reconciled value was unchanged. As the overlay's GeometryReader/preferences converge this runs several times per frame, and the redundant write to the observed property is what trips the AnyTextLayoutCollection diagnostic. It now only assigns when the value actually changes.

Testing

  • Textual target builds cleanly (xcodebuild -scheme Textual -destination 'platform=macOS' → BUILD SUCCEEDED).
  • Verified in a consuming macOS app (markdown rendered via StructuredText(markdown:).textual.textSelection(.enabled)): all three faults are gone from the console — both at launch and during interaction — with selection highlighting still rendering and reconciling correctly across layout/selection changes. The earlier handler-merge revision removed the two ... multiple times per frame faults but surfaced Modifying state during view update; the computed-property approach in this PR removes that last one as well.

I didn't find an existing harness for exercising the SwiftUI selection layout in tests — happy to add coverage if you can point me at the preferred approach for these view-level interactions.

With text selection enabled, SwiftUI logged non-fatal faults on layout
passes:

  onChange(of: Layout) action tried to update multiple times per frame.
  onChange(of: AnyTextLayoutCollection) action tried to update multiple times per frame.
  Modifying state during view update, this will cause undefined behavior.

`AppKitTextSelectionView` stored selection rectangles in `@State` and
seeded them from two `onChange(initial: true)` handlers (`selectedRange`
and `layout`), both writing the same state during the initial view
update. Derive the rectangles as an `@Observable`-tracked computed
property instead: no `@State`, no `onChange`, no state mutation during
the update, while still recomputing whenever the selected range or layout
changes.

`TextSelectionModel.setLayoutCollection` re-wrote the observed
`selectedRange` during reconciliation even when the reconciled value was
unchanged; as the overlay's GeometryReader/preferences converge this runs
several times per frame. Only assign when the value actually changes.

Fixes gonzalezreal#22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adamtheturtle adamtheturtle force-pushed the fix/selection-multiple-updates-per-frame branch from 4561138 to d7e8e12 Compare June 15, 2026 17:27
@adamtheturtle adamtheturtle changed the title Fix multiple-updates-per-frame faults in text selection Fix state-during-view-update faults in text selection Jun 15, 2026
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.

[Minor Issue] Potential multiple updates per frame in StructuredText(markdown:) view

1 participant