iCloud Drive writes now flow through a persistent WriteBacklog sidecar (drained with backoff, flushed on backgrounding, wiped with the cache on account change), so a save can never be lost to a transient coordinator error. A status banner on the workout list surfaces stuck syncing. Also: the split picker gains a Recent section with day labels, split rows fold SplitItem into SplitListView, and list rows dim the multiply sign in sets-by-reps. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
119 lines
3.7 KiB
Swift
119 lines
3.7 KiB
Swift
//
|
|
// SplitListView.swift
|
|
// Workouts
|
|
//
|
|
// Created by rzen on 7/25/25 at 6:24 PM.
|
|
//
|
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
/// The Splits library screen, pushed from Settings → Library: a plain list of
|
|
/// splits, one row each. Tapping a row opens `SplitDetailView`; long-pressing
|
|
/// offers Delete (the sole split-delete affordance); the toolbar's + adds a new
|
|
/// split. Starter splits are seeded automatically on the true first run
|
|
/// (`SyncEngine.autoSeedIfEmpty`), so an empty library just invites creating one.
|
|
struct SplitListView: View {
|
|
@Environment(SyncEngine.self) private var sync
|
|
|
|
@Query(sort: \Split.name) private var splits: [Split]
|
|
|
|
@State private var showingAddSheet = false
|
|
@State private var splitToDelete: Split?
|
|
|
|
var body: some View {
|
|
List {
|
|
ForEach(splits) { split in
|
|
NavigationLink {
|
|
SplitDetailView(split: split)
|
|
} label: {
|
|
SplitRow(split: split)
|
|
}
|
|
.contextMenu {
|
|
Button(role: .destructive) {
|
|
splitToDelete = split
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.overlay {
|
|
if splits.isEmpty {
|
|
ContentUnavailableView(
|
|
"No Splits Yet",
|
|
systemImage: "dumbbell.fill",
|
|
description: Text("Create a split to organize your workout routine.")
|
|
)
|
|
}
|
|
}
|
|
.navigationTitle("Splits")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button {
|
|
showingAddSheet = true
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
.accessibilityLabel("Add Split")
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingAddSheet) {
|
|
SplitAddEditView(split: nil)
|
|
}
|
|
.confirmationDialog(
|
|
"Delete Split?",
|
|
isPresented: Binding(
|
|
get: { splitToDelete != nil },
|
|
set: { if !$0 { splitToDelete = nil } }
|
|
),
|
|
titleVisibility: .visible,
|
|
presenting: splitToDelete
|
|
) { split in
|
|
Button("Delete", role: .destructive) {
|
|
Task { await sync.delete(split: split) }
|
|
splitToDelete = nil
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
splitToDelete = nil
|
|
}
|
|
} message: { split in
|
|
Text("This will permanently delete \"\(split.name)\" and all its exercises. Past workouts are kept.")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A single split row: a small rounded-square badge (the split's color at low
|
|
/// opacity, its symbol tinted full-strength) beside the split name and its
|
|
/// exercise count.
|
|
private struct SplitRow: View {
|
|
var split: Split
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: split.systemImage)
|
|
.font(.title3)
|
|
.foregroundStyle(splitColor)
|
|
.frame(width: 36, height: 36)
|
|
.background(splitColor.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(split.name)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text("\(split.exercisesArray.count) exercises")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
|
|
private var splitColor: Color {
|
|
Color.color(from: split.color)
|
|
}
|
|
}
|