Groundwork for the Library tab: RoutineOrdering.changedOrders turns an .onMove gesture into the minimal set of order writes (normalizing legacy colliding orders on first drag), RoutineNaming.uniqueName picks the next free "X Copy" name for duplication, and ExerciseCatalog groups the bundled exercise library into curated category sections with name/category/target search. Plus ExerciseDocument.planSummary for read-only document rendering. All covered by unit tests, including a motion/info name-parity assertion. Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
53 lines
2.2 KiB
Swift
53 lines
2.2 KiB
Swift
//
|
|
// RoutineOrdering.swift
|
|
// Workouts
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
/// Pure reordering math shared by any list backed by `.onMove` + a `SyncEngine`
|
|
/// document write: given the row's current ids and a SwiftUI move gesture, compute
|
|
/// the renumbered `order` values and hand back only the ones that actually changed,
|
|
/// so a caller writes the smallest possible document diff.
|
|
enum RoutineOrdering {
|
|
/// Apply SwiftUI's `.onMove` semantics to `ids` (`Array.move(fromOffsets:toOffset:)`),
|
|
/// renumber the result 0…n-1, and return only the `(id, newOrder)` pairs whose order
|
|
/// differs from `currentOrders[id]`. An id absent from `currentOrders` always comes
|
|
/// back (there's nothing to compare against, so it can't be assumed unchanged).
|
|
///
|
|
/// Comparing every renumbered position against the stored value — rather than only
|
|
/// the ones the drag actually touched — also normalizes legacy colliding order
|
|
/// values (e.g. two exercises both stored at order 0) the first time the list is
|
|
/// dragged: every row gets its correct 0…n-1 position written, not just the moved one.
|
|
static func changedOrders(
|
|
ids: [String], currentOrders: [String: Int], from source: IndexSet, to destination: Int
|
|
) -> [(id: String, order: Int)] {
|
|
var reordered = ids
|
|
reordered.move(fromOffsets: source, toOffset: destination)
|
|
|
|
return reordered.enumerated().compactMap { index, id in
|
|
currentOrders[id] == index ? nil : (id: id, order: index)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Naming helper for the "duplicate this routine" action.
|
|
enum RoutineNaming {
|
|
/// The next free "«base» Copy" name: `"«base» Copy"` when that's unused, else
|
|
/// `"«base» Copy 2"`, `"«base» Copy 3"`, … — skipping any name already present in
|
|
/// `existing` (case-sensitive match).
|
|
static func uniqueName(base: String, existing: Set<String>) -> String {
|
|
let first = "\(base) Copy"
|
|
guard existing.contains(first) else { return first }
|
|
|
|
var suffix = 2
|
|
while existing.contains("\(base) Copy \(suffix)") {
|
|
suffix += 1
|
|
}
|
|
return "\(base) Copy \(suffix)"
|
|
}
|
|
}
|