From 8cf145781403287b2a08da336822487e996de171 Mon Sep 17 00:00:00 2001 From: rzen Date: Sat, 8 Aug 2026 12:21:35 -0400 Subject: [PATCH] =?UTF-8?q?The=20phone's=20list=20learns=20a=20second=20or?= =?UTF-8?q?der=20=E2=80=94=20sort=20boards=20by=20name=20or=20by=20recency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A segmented control above the mobile board list, remembered across launches: Name keeps the index's stable title order, Recent sorts by content-change date descending with undated boards last and ties held stable against refresh reshuffling. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY --- KanbanMobile/CHANGELOG.md | 2 + KanbanMobile/Cloud/BoardSummary.swift | 40 +++++++++++++++++++ KanbanMobile/Screens/BoardsTabView.swift | 30 +++++++++++++- ...ts.swift => GitAgnosticStorageTests.swift} | 0 4 files changed, 70 insertions(+), 2 deletions(-) rename KanbanTests/{InertGitTests.swift => GitAgnosticStorageTests.swift} (100%) diff --git a/KanbanMobile/CHANGELOG.md b/KanbanMobile/CHANGELOG.md index 7055ba8..a228f67 100644 --- a/KanbanMobile/CHANGELOG.md +++ b/KanbanMobile/CHANGELOG.md @@ -1,5 +1,7 @@ **August 2026** +Sort your board list by name or by most recently changed with the new control above the list. + Version 1.0: Lanework comes to iPhone — browse your boards from iCloud Drive, move cards between lanes, and edit them on the go. Swipe a board in the list to open its settings and move it between iCloud and this iPhone, which now works without an iCloud account. diff --git a/KanbanMobile/Cloud/BoardSummary.swift b/KanbanMobile/Cloud/BoardSummary.swift index 9c08822..0f587b0 100644 --- a/KanbanMobile/Cloud/BoardSummary.swift +++ b/KanbanMobile/Cloud/BoardSummary.swift @@ -35,6 +35,46 @@ struct BoardSummary: Identifiable, Sendable, Equatable { let location: BoardLocation } +/// How the boards tab orders its merged list. Raw values are persisted (`AppStorage`), so they are +/// API — changing a case's raw value would silently reset every user's remembered choice. +enum BoardSortOrder: String, CaseIterable, Sendable { + case name + case recent +} + +extension BoardSortOrder { + /// Orders `boards` for display without touching the two-home merge that produced them. + /// + /// **`.name` is a no-op.** `BoardIndexStore` already publishes its list in title order — + /// case/diacritic-insensitive, path as the tie-break — so re-sorting here would just repeat work + /// already done upstream. + /// + /// **`.recent` sorts by `modified` descending, with `nil` last.** Ties — equal dates, and every + /// `nil` among them — keep the incoming order: Swift's `sort` is not a stable sort, so the + /// comparator breaks ties on each board's original offset rather than leaving equal rows free to + /// swap places on every refresh. + nonisolated func sorted(_ boards: [BoardSummary]) -> [BoardSummary] { + switch self { + case .name: + return boards + + case .recent: + return boards.enumerated().sorted { lhs, rhs in + switch (lhs.element.modified, rhs.element.modified) { + case let (l?, r?) where l != r: + return l > r + case (nil, .some): + return false + case (.some, nil): + return true + default: + return lhs.offset < rhs.offset + } + }.map(\.element) + } + } +} + /// Where a board is stored, and therefore whether it syncs. /// /// **Told, never sniffed.** A location is what the home an entry was enumerated from *is*, so the diff --git a/KanbanMobile/Screens/BoardsTabView.swift b/KanbanMobile/Screens/BoardsTabView.swift index 73f596f..c325c3f 100644 --- a/KanbanMobile/Screens/BoardsTabView.swift +++ b/KanbanMobile/Screens/BoardsTabView.swift @@ -6,12 +6,17 @@ import SwiftUI /// Renders both of `BoardIndexStore.Phase`'s live states and, above the list, the iCloud notice — /// which is a row, not a wall (softened 2026-08-08). A phone with no account still has a device home /// and therefore still has boards, so the missing half of the app is reported next to the half that -/// works rather than in place of it. +/// works rather than in place of it. A segmented control above the rows lets the list be sorted by +/// name or by most recent change, remembered across launches. struct BoardsTabView: View { @Environment(BoardIndexStore.self) private var index @State private var isPresentingNewBoard = false + /// Persisted as its raw value, not the enum itself — `AppStorage` needs a property-list type, and + /// the raw `String` is exactly what `BoardSortOrder` promises to keep stable. + @AppStorage("boardListSortOrder") private var sortOrderRaw = BoardSortOrder.name.rawValue + /// The board whose settings sheet is up. A value, not a URL: the sheet renders the row's own /// summary, and a successful move dismisses it before the stale copy could matter. @State private var boardInSettings: BoardSummary? @@ -55,6 +60,19 @@ struct BoardsTabView: View { } } + /// The stored raw value as the enum, defaulting to `.name` on anything the store didn't write + /// itself — an unset key or a stale raw value from a build that no longer has this case. + private var sortOrder: Binding { + Binding( + get: { BoardSortOrder(rawValue: sortOrderRaw) ?? .name }, + set: { sortOrderRaw = $0.rawValue } + ) + } + + private var sortedBoards: [BoardSummary] { + sortOrder.wrappedValue.sorted(index.boards) + } + @ViewBuilder private var content: some View { switch index.phase { @@ -75,7 +93,15 @@ struct BoardsTabView: View { ) } } else { - ForEach(index.boards) { board in + Picker("Sort", selection: sortOrder) { + Text("Name").tag(BoardSortOrder.name) + Text("Recent").tag(BoardSortOrder.recent) + } + .pickerStyle(.segmented) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + + ForEach(sortedBoards) { board in NavigationLink(value: BoardRoute.lanes(boardRoot: board.rootURL)) { BoardSummaryRow(board: board) } diff --git a/KanbanTests/InertGitTests.swift b/KanbanTests/GitAgnosticStorageTests.swift similarity index 100% rename from KanbanTests/InertGitTests.swift rename to KanbanTests/GitAgnosticStorageTests.swift