Skip to content

Fix crash: replace usesAutomaticRowHeights with deferred self-sizing#85

Draft
beezly wants to merge 1 commit intoviktorstrate:mainfrom
beezly:fix/deferred-row-sizing
Draft

Fix crash: replace usesAutomaticRowHeights with deferred self-sizing#85
beezly wants to merge 1 commit intoviktorstrate:mainfrom
beezly:fix/deferred-row-sizing

Conversation

@beezly
Copy link
Contributor

@beezly beezly commented Mar 17, 2026

Problem

NSHostingView inside NSTableView with usesAutomaticRowHeights enters an infinite layout loop when SwiftUI view content changes during layout. AppKit detects this and crashes with NSGenericException:

The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window.

The crash reproduces when scrolling into rooms containing video/image messages with reactions or read receipts. It is timing-dependent — more likely when launched from Finder than under a debugger.

Root Cause

During layout, NSHostingView.layout() flushes SwiftUI transactions. If the SwiftUI view tree has changed (e.g. async content loaded), this calls invalidateIntrinsicContentSize, which triggers constraint updates, which re-enters layout — exceeding AppKit's constraint update pass limit (one pass per view in the window).

This was latent since the NSTableView rewrite (#47) but became reliably triggerable as view complexity grew (reactions, read receipts, video playback).

Fix

Replace the entire auto-sizing mechanism with SelfSizingHostingView:

  • sizingOptions = [] — prevents the hosting view from participating in Auto Layout constraint solving
  • invalidateIntrinsicContentSize() overridden to schedule a coalesced async height update instead of calling super (which would re-enter the constraint system)
  • measureHeight() temporarily re-enables sizing, pins a width constraint matching the column width, reads fittingSize, then resets — all outside the layout pass
  • heightOfRow returns cached heights (default 60px); noteHeightOfRows called with animation duration 0

Also removes the old measurementHostingView (separate NSHostingController per height query) and handleTableResize observer.

Depends on

@beezly beezly force-pushed the fix/deferred-row-sizing branch 2 times, most recently from f959378 to 019121b Compare March 17, 2026 11:05
NSHostingView inside NSTableView with usesAutomaticRowHeights enters an
infinite layout loop: layout() flushes SwiftUI transactions, which
invalidate constraints, which re-enter layout. AppKit detects this and
crashes with NSGenericException.

Replace the entire auto-sizing mechanism with SelfSizingHostingView:

- sizingOptions = [] prevents the hosting view from participating in
  Auto Layout constraint solving.

- invalidateIntrinsicContentSize() is overridden to NOT call super
  (which would re-enter the constraint system) but instead schedule
  a coalesced async height update via DispatchQueue.main.async.

- measureHeight() uses a temporary NSHostingController.sizeThatFits()
  to properly measure content at the cell width, including text wrapping.

- heightOfRow returns cached heights (default 60px). noteHeightOfRows
  is called with animation duration 0 to avoid visible row resizing.

Also removes the old measurementHostingView and handleTableResize.
@viktorstrate
Copy link
Owner

It's hard for me to understand what this solves because I can not reproduce the original problem. The async trick seems brittle and complicated, and I think it could quickly lead to new and harder problems.

When I scroll quickly around I can see that the heights jump because they're first layed out wrongly and then corrected in the next frame. This is both visually distracting and could potentially hurt performance since the layout needs to be calculated twice.

I think a better approach would be to make sure that SwiftUI doesn't asynchronously change the structure in a way that causes these problems. Alternatively, I am also open for rewriting TimelineItemRowView in AppKit with the use of hosting views eg. for the avatar view and reactions view. But since the text is already AppKit, having the root row be an AppKit view as well, might actually simplify things as we would have precise controls over the layout constraints. What do you think about all this?

A complete sidenote: I have noticed that if I make the window too small in width, the app hangs, but this is completely unrelated to this PR, since it happens both before and after these changes.

@beezly
Copy link
Contributor Author

beezly commented Mar 19, 2026

This problem occurs with a test room that I have on my homeserver that has a lot of video content with slightly strange things like missing thumbnails, etc and it's that room that seems to provoke the crash for me (usually instantly). All that said, I agree that appkit with hosting views is a much better long term approach. Let me see what it takes to pivot in that direction.

@beezly beezly marked this pull request as draft March 19, 2026 14:55
@viktorstrate
Copy link
Owner

It sounds to me that this could maybe also be solved from SwiftUI, eg by tweaking the code for the video message view or alike

@beezly
Copy link
Contributor Author

beezly commented Mar 19, 2026

I've built a branch which uses AppKit instead of SwiftUI components: https://github.com/beezly/mactrix/tree/appkit-timeline-row.

One significant difference from before, the old code used a measurementHostingView to compute exact heights for state/virtual rows (expensive - created a whole SwiftUI layout pass per row).
Now everything uses cheap constant estimates, and Auto Layout corrects them. For virtual items, the estimate is exact (40pt matches the constraint), so there's zero correction needed.

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