Fix crash: replace usesAutomaticRowHeights with deferred self-sizing#85
Fix crash: replace usesAutomaticRowHeights with deferred self-sizing#85beezly wants to merge 1 commit intoviktorstrate:mainfrom
Conversation
f959378 to
019121b
Compare
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.
019121b to
e27d085
Compare
|
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 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. |
|
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. |
|
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 |
|
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). |
Problem
NSHostingViewinsideNSTableViewwithusesAutomaticRowHeightsenters an infinite layout loop when SwiftUI view content changes during layout. AppKit detects this and crashes withNSGenericException: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 callsinvalidateIntrinsicContentSize, 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 solvinginvalidateIntrinsicContentSize()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, readsfittingSize, then resets — all outside the layout passheightOfRowreturns cached heights (default 60px);noteHeightOfRowscalled with animation duration 0Also removes the old
measurementHostingView(separateNSHostingControllerper height query) andhandleTableResizeobserver.Depends on