The phone's list learns a second order — sort boards by name or by recency
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
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
**August 2026**
|
**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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -35,6 +35,46 @@ struct BoardSummary: Identifiable, Sendable, Equatable {
|
|||||||
let location: BoardLocation
|
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.
|
/// 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
|
/// **Told, never sniffed.** A location is what the home an entry was enumerated from *is*, so the
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import SwiftUI
|
|||||||
/// Renders both of `BoardIndexStore.Phase`'s live states and, above the list, the iCloud notice —
|
/// 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
|
/// 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
|
/// 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 {
|
struct BoardsTabView: View {
|
||||||
@Environment(BoardIndexStore.self) private var index
|
@Environment(BoardIndexStore.self) private var index
|
||||||
|
|
||||||
@State private var isPresentingNewBoard = false
|
@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
|
/// 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.
|
/// summary, and a successful move dismisses it before the stale copy could matter.
|
||||||
@State private var boardInSettings: BoardSummary?
|
@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<BoardSortOrder> {
|
||||||
|
Binding(
|
||||||
|
get: { BoardSortOrder(rawValue: sortOrderRaw) ?? .name },
|
||||||
|
set: { sortOrderRaw = $0.rawValue }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sortedBoards: [BoardSummary] {
|
||||||
|
sortOrder.wrappedValue.sorted(index.boards)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
switch index.phase {
|
switch index.phase {
|
||||||
@@ -75,7 +93,15 @@ struct BoardsTabView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)) {
|
NavigationLink(value: BoardRoute.lanes(boardRoot: board.rootURL)) {
|
||||||
BoardSummaryRow(board: board)
|
BoardSummaryRow(board: board)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user