From 50a55d9045fd072b4172b4d7c8a56891dc889ddf Mon Sep 17 00:00:00 2001 From: Sergio Esteban Date: Fri, 15 May 2026 12:16:41 +0200 Subject: [PATCH] fix(ui): prevent LazyVStack layout hang with 700+ cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar ListBoardView used LazyVStack with pinnedViews: [.sectionHeaders], but each section wrapped its cards in an inner LazyVStack. The outer container needed to measure each section to position pinned headers, which required materializing the inner LazyVStack — forcing all ~700 card views to be laid out on every reconciliation cycle. Thread samples confirmed 100% main thread time spent in LazySubviewPlacements.placeSubviews → ForEachList.applyNodes. Two changes: - Remove pinnedViews from the outer LazyVStack so sections are lazily evaluated without full measurement - Replace the inner LazyVStack with VStack so the outer container can size sections via their direct children Also move ChannelShareController drain loops from Task.detached to DispatchQueue.global to avoid blocking the cooperative thread pool with synchronous FileHandle.availableData reads. --- Sources/KanbanCode/ChannelShareController.swift | 5 ++--- Sources/KanbanCode/ListBoardView.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/KanbanCode/ChannelShareController.swift b/Sources/KanbanCode/ChannelShareController.swift index 1d9faf4..7564b0d 100644 --- a/Sources/KanbanCode/ChannelShareController.swift +++ b/Sources/KanbanCode/ChannelShareController.swift @@ -230,12 +230,11 @@ final class ChannelShareController { } private static func drainStderr(_ handle: FileHandle, tag: String) { - Task.detached { + DispatchQueue.global(qos: .utility).async { while true { let data = handle.availableData if data.isEmpty { return } if let s = String(data: data, encoding: .utf8) { - // Keep share-related diagnostics quiet unless debugging. FileHandle.standardError.write(Data("\(tag) \(s)".utf8)) } } @@ -243,7 +242,7 @@ final class ChannelShareController { } private static func drainStdout(_ handle: FileHandle, tag: String) { - Task.detached { + DispatchQueue.global(qos: .utility).async { while true { let data = handle.availableData if data.isEmpty { return } diff --git a/Sources/KanbanCode/ListBoardView.swift b/Sources/KanbanCode/ListBoardView.swift index a2ded9e..f16b235 100644 --- a/Sources/KanbanCode/ListBoardView.swift +++ b/Sources/KanbanCode/ListBoardView.swift @@ -61,7 +61,7 @@ struct ListBoardView: View { private func scrollView(proxy: ScrollViewProxy) -> some View { ScrollView { - LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { + LazyVStack(alignment: .leading, spacing: 0) { channelsSection ForEach(sections, id: \.column) { section in sectionView(for: section) @@ -350,7 +350,7 @@ private struct ListBoardSectionView: View { onReorderCard: onReorderCard )) } else { - LazyVStack(spacing: 4) { + VStack(spacing: 4) { ForEach(section.cards) { card in if dragState.reorderTargetId == card.id && dragState.reorderAbove { ReorderIndicator()