summaryrefslogtreecommitdiff
path: root/Hutch/Views/Builds/BuildListViewModel.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 13:35:03 -0500
committerChristian Cleberg <[email protected]>2026-04-13 13:35:03 -0500
commit0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50 (patch)
treec2d92448661d9e6ca76bd7c69e9d51131305214e /Hutch/Views/Builds/BuildListViewModel.swift
parent06b24715cd244475ded5482e926721a57278a3cf (diff)
downloadhutch-0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50.tar.gz
hutch-0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50.tar.bz2
hutch-0b84ecaf7ba0fb2cd3f3867da8dc732450b55b50.zip
perf: cache filtered collections and add equatable row views
Convert filteredJobs, filteredPastes, and filteredTickets from computed properties to stored properties updated via didSet on their inputs. Each update function guards with an equality check before assigning, so @Observable skips the notification when the filtered result hasn't changed — preventing list re-renders on auto-refresh when no visible data has actually changed. Add Equatable conformance to BuildRowView, PasteRowView, TicketRowView, and SelectableTicketRow, and apply .equatable() at each ForEach call site. When a list does re-render, SwiftUI now skips body evaluation for rows whose model value is identical to the previous pass. Implements: https://todo.sr.ht/~ccleberg/hutch/57
Diffstat (limited to 'Hutch/Views/Builds/BuildListViewModel.swift')
-rw-r--r--Hutch/Views/Builds/BuildListViewModel.swift35
1 files changed, 27 insertions, 8 deletions
diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift
index c6f8e49..643bacd 100644
--- a/Hutch/Views/Builds/BuildListViewModel.swift
+++ b/Hutch/Views/Builds/BuildListViewModel.swift
@@ -46,18 +46,33 @@ enum AutoRefreshInterval: Int, CaseIterable, Sendable {
final class BuildListViewModel {
private static let searchHistoryScopeID = "builds"
- private(set) var jobs: [JobSummary] = []
+ private(set) var jobs: [JobSummary] = [] {
+ didSet { updateFilteredJobs() }
+ }
private(set) var recentSearches: [ScopedSearchHistoryEntry]
private(set) var isLoading = false
private(set) var isLoadingMore = false
private(set) var isRefreshing = false
private(set) var isSubmitting = false
var error: String?
- var filter: BuildListFilter = .attention
- var searchText = ""
+ var filter: BuildListFilter = .attention {
+ didSet { updateFilteredJobs() }
+ }
+ var searchText = "" {
+ didSet { updateFilteredJobs() }
+ }
var repoFilter: String = "" {
- didSet { if repoFilter != oldValue { repoFilterDidChange() } }
+ didSet {
+ guard repoFilter != oldValue else { return }
+ repoFilterDidChange()
+ updateFilteredJobs()
+ }
}
+ // Cached filtered result. Updated whenever jobs, filter, searchText, or
+ // repoFilter changes. Only notifies observers when the content actually
+ // differs, which prevents the list from re-rendering on auto-refresh when
+ // no visible data changed.
+ private(set) var filteredJobs: [JobSummary] = []
private var cursor: String?
private var hasMore = true
@@ -83,14 +98,18 @@ final class BuildListViewModel {
return allTags.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
- var filteredJobs: [JobSummary] {
+ private func updateFilteredJobs() {
var result = Self.filterJobs(jobs, filter: filter)
-
if !repoFilter.isEmpty {
result = result.filter { $0.tags.contains(repoFilter) }
}
-
- return Self.searchJobs(result, matching: searchText)
+ let updated = Self.searchJobs(result, matching: searchText)
+ // Skip the assignment (and the resulting observer notification) when the
+ // filtered list hasn't actually changed — e.g. on auto-refresh when no
+ // builds have been added or updated.
+ if updated != filteredJobs {
+ filteredJobs = updated
+ }
}
// MARK: - Auto-Refresh