@@ -87,6 +87,8 @@ class TimelineViewController: NSViewController {
8787
8888 let timeline : LiveTimeline
8989 var timelineItems : [ TimelineItem ]
90+ var rowHeights : [ Int : CGFloat ] = [ : ]
91+ private var pendingHeightUpdate = false
9092
9193 init ( coordinator: TimelineViewRepresentable . Coordinator , timeline: LiveTimeline , timelineItems: [ TimelineItem ] ) {
9294 self . coordinator = coordinator
@@ -104,30 +106,29 @@ class TimelineViewController: NSViewController {
104106 tableView. allowsColumnSelection = false
105107 tableView. selectionHighlightStyle = . none
106108
107- tableView. rowHeight = - 1
108- tableView. usesAutomaticRowHeights = true
109-
110- oldWidth = tableView. frame. width
109+ tableView. usesAutomaticRowHeights = false
110+ tableView. rowHeight = 60
111111
112112 dataSource = . init( tableView: tableView) { [ weak self] tableView, _, row, _ in
113113 guard let self else { return NSView ( ) }
114114
115115 let item = timelineItems [ row]
116116 let view = TimelineItemRowView ( rowInfo: item. rowInfo, timeline: timeline, coordinator: coordinator)
117117
118- let hostView : NSHostingView < TimelineItemRowView >
118+ let hostView : SelfSizingHostingView < TimelineItemRowView >
119119 if let recycledView = tableView. makeView ( withIdentifier: item. rowInfo. reuseIdentifier, owner: self )
120- as? NSHostingView < TimelineItemRowView >
120+ as? SelfSizingHostingView < TimelineItemRowView >
121121 {
122122 recycledView. rootView = view
123123 hostView = recycledView
124124 } else {
125- hostView = NSHostingView < TimelineItemRowView > ( rootView: view)
125+ hostView = SelfSizingHostingView < TimelineItemRowView > ( rootView: view)
126126 hostView. identifier = item. rowInfo. reuseIdentifier
127+ hostView. sizingOptions = [ ]
127128 hostView. autoresizingMask = [ . width, . height]
128- hostView. sizingOptions = [ . preferredContentSize]
129- hostView. setContentHuggingPriority ( . required, for: . vertical)
130129 }
130+ hostView. row = row
131+ hostView. controller = self
131132
132133 return hostView
133134 }
@@ -142,13 +143,6 @@ class TimelineViewController: NSViewController {
142143
143144 // Subscribe to view resize notifications
144145 scrollView. contentView. postsBoundsChangedNotifications = true
145- NotificationCenter . default. addObserver (
146- self ,
147- selector: #selector( handleTableResize) ,
148- name: NSView . frameDidChangeNotification,
149- object: scrollView. contentView
150- )
151-
152146 NotificationCenter . default. addObserver (
153147 self ,
154148 selector: #selector( viewDidScroll ( _: ) ) ,
@@ -159,21 +153,6 @@ class TimelineViewController: NSViewController {
159153 listenForFocusTimelineItem ( )
160154 }
161155
162- @objc func handleTableResize( _ notification: Notification ) {
163- if oldWidth != tableView. frame. width {
164- oldWidth = tableView. frame. width
165-
166- NSAnimationContext . runAnimationGroup { context in
167- context. duration = 0
168- context. allowsImplicitAnimation = false
169-
170- let visibleRect = tableView. visibleRect
171- let visibleRows = tableView. rows ( in: visibleRect)
172- tableView. noteHeightOfRows ( withIndexesChanged: IndexSet ( integersIn: visibleRows. lowerBound ..< visibleRows. upperBound) )
173- }
174- }
175- }
176-
177156 var timelineFetchTask : Task < Void , Never > ?
178157
179158 @objc func viewDidScroll( _ notification: Notification ) {
@@ -252,18 +231,10 @@ class TimelineViewController: NSViewController {
252231 // Re-measure visible rows after hosting views settle
253232 DispatchQueue . main. async { [ weak self] in
254233 guard let self else { return }
255- let visibleRows = tableView . rows ( in : tableView . visibleRect )
256- tableView . noteHeightOfRows ( withIndexesChanged : IndexSet ( integersIn : visibleRows . lowerBound ..< visibleRows . upperBound ) )
234+ self . rowHeights . removeAll ( )
235+ self . scheduleHeightUpdate ( )
257236 }
258237 }
259-
260- // values used to track width changes
261- var oldWidth : CGFloat ?
262- let measurementHostingView = {
263- let hostView = NSHostingController ( rootView: AnyView ( EmptyView ( ) ) )
264- hostView. sizingOptions = [ . preferredContentSize]
265- return hostView
266- } ( )
267238}
268239
269240extension TimelineViewController : NSTableViewDelegate {
@@ -276,16 +247,36 @@ extension TimelineViewController: NSTableViewDelegate {
276247 }
277248
278249 func tableView( _ tableView: NSTableView , heightOfRow row: Int ) -> CGFloat {
279- let item = timelineItems [ row]
280-
281- measurementHostingView. rootView = AnyView ( TimelineItemRowView ( rowInfo: item. rowInfo, timeline: timeline, coordinator: coordinator) )
250+ return rowHeights [ row] ?? 60
251+ }
282252
283- let targetWidth = tableView. tableColumns [ 0 ] . width
284- let proposedSize = CGSize ( width: targetWidth, height: CGFloat . greatestFiniteMagnitude)
253+ func scheduleHeightUpdate( ) {
254+ guard !pendingHeightUpdate else { return }
255+ pendingHeightUpdate = true
256+ DispatchQueue . main. async { [ weak self] in
257+ self ? . pendingHeightUpdate = false
258+ self ? . updateVisibleRowHeights ( )
259+ }
260+ }
285261
286- let size = measurementHostingView. sizeThatFits ( in: proposedSize)
287- // Avoid undefined-height rows which can cause NSTableView layout issues
288- return max ( size. height, 1 )
262+ private func updateVisibleRowHeights( ) {
263+ let visibleRows = tableView. rows ( in: tableView. visibleRect)
264+ var changed = IndexSet ( )
265+ for row in visibleRows. lowerBound..< visibleRows. upperBound {
266+ guard let cellView = tableView. view ( atColumn: 0 , row: row, makeIfNecessary: false )
267+ as? SelfSizingHostingView < TimelineItemRowView > else { continue }
268+ let h = cellView. measureHeight ( )
269+ if h > 0 , abs ( h - ( rowHeights [ row] ?? 60 ) ) > 1 {
270+ rowHeights [ row] = h
271+ changed. insert ( row)
272+ }
273+ }
274+ if !changed. isEmpty {
275+ NSAnimationContext . beginGrouping ( )
276+ NSAnimationContext . current. duration = 0
277+ tableView. noteHeightOfRows ( withIndexesChanged: changed)
278+ NSAnimationContext . endGrouping ( )
279+ }
289280 }
290281}
291282
@@ -295,3 +286,28 @@ class BottomStickyTableView: NSTableView {
295286 return false
296287 }
297288}
289+
290+ class SelfSizingHostingView < Content: View > : NSHostingView < Content > {
291+ weak var controller : TimelineViewController ?
292+ var row : Int = 0
293+
294+ override func layout( ) {
295+ super. layout ( )
296+ controller? . scheduleHeightUpdate ( )
297+ }
298+
299+ override func invalidateIntrinsicContentSize( ) {
300+ // Don't call super — prevents constraint loop
301+ // Instead schedule a deferred height update
302+ controller? . scheduleHeightUpdate ( )
303+ }
304+
305+ func measureHeight( ) -> CGFloat {
306+ let width = frame. width
307+ guard width > 0 else { return 0 }
308+ let controller = NSHostingController ( rootView: rootView)
309+ controller. sizingOptions = [ . preferredContentSize]
310+ let size = controller. sizeThatFits ( in: CGSize ( width: width, height: CGFloat . greatestFiniteMagnitude) )
311+ return max ( size. height, 1 )
312+ }
313+ }
0 commit comments