From 06b24715cd244475ded5482e926721a57278a3cf Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 13 Apr 2026 13:26:08 -0500 Subject: perf: reduce redundant fetches and improve list render efficiency - Remove no-op per-row task from RepositoryListView; loadMoreIfNeeded is a stub for repos so each row was allocating a Task that did nothing - Fix BuildListView auto-refresh stopping permanently after navigating away; startAutoRefresh now runs unconditionally on task so it restarts on every reappear, not just first load - Add lastRefreshed tracking to HomeViewModel with a needsRefresh(after:) helper; HomeView and WorkView scene-activation handlers now skip loadDashboard() if the data is less than 60 seconds old - Add 120-second TTL to repository build status refresh; statuses are no longer re-fetched on every tab appear, only when stale or when the user explicitly pulls to refresh (forceRefresh: true) Implements: https://todo.sr.ht/~ccleberg/hutch/56 --- Hutch/Views/Repositories/RepositoryListViewModel.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'Hutch/Views/Repositories/RepositoryListViewModel.swift') diff --git a/Hutch/Views/Repositories/RepositoryListViewModel.swift b/Hutch/Views/Repositories/RepositoryListViewModel.swift index 0cfc8c3..1e830bb 100644 --- a/Hutch/Views/Repositories/RepositoryListViewModel.swift +++ b/Hutch/Views/Repositories/RepositoryListViewModel.swift @@ -46,6 +46,7 @@ final class RepositoryListViewModel { private let client: SRHTClient private let defaults: UserDefaults private var buildStatusTask: Task? + private var lastBuildStatusRefresh: Date? private static let gitCacheKey = "git.repositories" private static let hgCacheKey = "hg.repositories" @@ -147,7 +148,8 @@ final class RepositoryListViewModel { /// Fetch the first page of repositories. Shows cached data instantly if available, /// then refreshes from the network in the background. /// - Parameter search: Optional search string. Pass `nil` to use the current `searchText`. - func loadRepositories(search: String? = nil) async { + /// - Parameter forceRefresh: When true, bypass the build-status TTL (e.g. pull-to-refresh). + func loadRepositories(search: String? = nil, forceRefresh: Bool = false) async { let query = (search ?? searchText).trimmingCharacters(in: .whitespacesAndNewlines) let isSearch = !query.isEmpty @@ -193,7 +195,7 @@ final class RepositoryListViewModel { } repositories = filteredResults.sorted(by: repositorySortOrder) - scheduleBuildStatusRefresh() + scheduleBuildStatusRefresh(force: forceRefresh) } catch { // Only show error if we have no cached data to fall back on if repositories.isEmpty { @@ -583,7 +585,13 @@ final class RepositoryListViewModel { } } - private func scheduleBuildStatusRefresh() { + private func scheduleBuildStatusRefresh(force: Bool = false) { + // Skip if we already refreshed recently (120-second TTL). Pull-to-refresh + // passes force: true to bypass this check. + if !force, let last = lastBuildStatusRefresh, + Date().timeIntervalSince(last) < 120 { + return + } let repositoriesSnapshot = repositories buildStatusTask?.cancel() buildStatusTask = Task { [weak self] in @@ -635,6 +643,7 @@ final class RepositoryListViewModel { await MainActor.run { guard repositories == self.repositories else { return } latestBuildStatuses = finalStatuses + lastBuildStatusRefresh = Date() } } catch { // Build status is auxiliary data for the list. Leave the default gray state on failure. -- cgit v1.2.3