Skip to content

Commit 0f444c3

Browse files
committed
perf: cache selectedCard to avoid CardDetailView re-renders
Add selectedCard as an equality-gated stored property on AppState, updated in rebuildCards(). ContentView now reads store.state.selectedCard instead of store.state.cards.first(where:) — this avoids observing the entire cards array just to get the selected card, so CardDetailView only re-renders when the selected card's data actually changes.
1 parent 8f79d6f commit 0f444c3

2 files changed

Lines changed: 12 additions & 5 deletions

File tree

Sources/KanbanCode/ContentView.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ struct ContentView: View {
549549

550550
@ViewBuilder
551551
private var inspectorContent: some View {
552-
if let card = store.state.cards.first(where: { $0.id == store.state.selectedCardId }) {
552+
if let card = store.state.selectedCard {
553553
makeCardDetailView(card: card)
554554
}
555555
}
@@ -573,7 +573,7 @@ struct ContentView: View {
573573
private var boardWithOverlays: some View {
574574
Group {
575575
if isExpandedDetail {
576-
if let card = store.state.cards.first(where: { $0.id == store.state.selectedCardId }) {
576+
if let card = store.state.selectedCard {
577577
makeCardDetailView(card: card)
578578
} else {
579579
expandedEmptyState
@@ -645,7 +645,7 @@ struct ContentView: View {
645645
}
646646
.animation(.easeInOut(duration: 0.15), value: isDroppingFolder)
647647
.overlay {
648-
if let card = store.state.cards.first(where: { $0.id == store.state.selectedCardId }),
648+
if let card = store.state.selectedCard,
649649
let sessionName = card.link.tmuxLink?.sessionName {
650650
ImageDropZone(isTargeted: $isDroppingImage) { imageData in
651651
NSPasteboard.general.clearContents()
@@ -1073,7 +1073,7 @@ struct ContentView: View {
10731073
}
10741074
}
10751075

1076-
if tbVis.showExpandedCardInfo, let card = store.state.cards.first(where: { $0.id == store.state.selectedCardId }) {
1076+
if tbVis.showExpandedCardInfo, let card = store.state.selectedCard {
10771077
ToolbarItemGroup(placement: .navigation) {
10781078
HStack {
10791079
Text("⠀⠀" + card.displayTitle)
@@ -1468,7 +1468,7 @@ struct ContentView: View {
14681468
// Fullscreen: never close the card with Esc
14691469
if isExpandedDetail { return }
14701470
// Chat mode + working: send interrupt instead of closing
1471-
if let card = store.state.cards.first(where: { $0.id == store.state.selectedCardId }),
1471+
if let card = store.state.selectedCard,
14721472
let session = card.link.tmuxLink?.sessionName,
14731473
card.activityState == .activelyWorking || card.activityState == .idleWaiting {
14741474
if UserDefaults.standard.bool(forKey: "preferChatView") {

Sources/KanbanCodeCore/UseCases/BoardStore.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ public final class AppState: @unchecked Sendable {
8080
/// Cached cards array — rebuilt by BoardStore after each dispatch.
8181
public internal(set) var cards: [KanbanCodeCard] = []
8282

83+
/// The currently selected card — independently tracked so CardDetailView
84+
/// only re-renders when the selected card's data actually changes.
85+
public internal(set) var selectedCard: KanbanCodeCard?
86+
8387
/// Cards visible after project filtering — cached for independent observation.
8488
public internal(set) var filteredCards: [KanbanCodeCard] = []
8589

@@ -101,6 +105,9 @@ public final class AppState: @unchecked Sendable {
101105
}
102106
if newCards != cards { cards = newCards }
103107

108+
let newSelected = selectedCardId.flatMap { id in cards.first { $0.id == id } }
109+
if newSelected != selectedCard { selectedCard = newSelected }
110+
104111
let newFiltered = cards.filter { cardMatchesProjectFilter($0) }
105112
if newFiltered != filteredCards { filteredCards = newFiltered }
106113

0 commit comments

Comments
 (0)