151 lines
5.0 KiB
Swift
151 lines
5.0 KiB
Swift
import SwiftUI
|
|
import HealthKit
|
|
import HealthDB
|
|
import SFSafeSymbols
|
|
|
|
struct WorkoutTab: View {
|
|
|
|
@EnvironmentObject
|
|
var database: Database
|
|
|
|
@State var navigationPath: NavigationPath = .init()
|
|
|
|
@State var filteredActivityType: HKWorkoutActivityType? = nil
|
|
|
|
@State var workouts: [(title: String, workouts: [Workout])] = []
|
|
|
|
@State var workoutTypeCounts: [(type: HKWorkoutActivityType, count: Int)] = []
|
|
|
|
var body: some View {
|
|
NavigationStack(path: $navigationPath) {
|
|
VStack {
|
|
List {
|
|
WorkoutTypeSelection(selected: $filteredActivityType, available: $workoutTypeCounts)
|
|
.listRowSeparator(.hidden)
|
|
ForEach(workouts, id: \.title) { month in
|
|
Section {
|
|
ForEach(month.workouts) { workout in
|
|
WorkoutListRow(workout: workout)
|
|
.overlay(
|
|
NavigationLink(value: workout) { }
|
|
.opacity(0))
|
|
.listRowSeparator(.hidden)
|
|
.listRowInsets(.init(top: 0, leading: 16, bottom: 5, trailing: 16))
|
|
}
|
|
} header: {
|
|
Text(month.title)
|
|
.font(.title3)
|
|
.fontWeight(.bold)
|
|
.foregroundStyle(.primary)
|
|
} footer: {
|
|
Text("")
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
.navigationTitle("Workouts")
|
|
.navigationDestination(for: Workout.self) {
|
|
WorkoutDetailView(workout: $0)
|
|
}
|
|
.refreshable {
|
|
reloadAsync()
|
|
}
|
|
.onChange(of: database.file, reload)
|
|
.onChange(of: filteredActivityType, reload)
|
|
.onAppear(perform: {
|
|
if workouts.isEmpty {
|
|
reload()
|
|
}
|
|
})
|
|
}
|
|
.preferredColorScheme(.dark)
|
|
}
|
|
|
|
private func reload() {
|
|
Task {
|
|
reloadAsync()
|
|
}
|
|
}
|
|
|
|
private func reloadAsync() {
|
|
guard let store = database.store else {
|
|
DispatchQueue.main.async {
|
|
self.workouts = []
|
|
self.workoutTypeCounts = []
|
|
}
|
|
return
|
|
}
|
|
loadWorkoutTypes(in: store)
|
|
loadWorkouts(in: store)
|
|
}
|
|
|
|
private func loadWorkouts(in store: HealthDatabase) {
|
|
do {
|
|
let workouts: [Workout]
|
|
if let filteredActivityType {
|
|
workouts = try store.workouts(type: filteredActivityType)
|
|
} else {
|
|
workouts = try store.workouts()
|
|
}
|
|
print("Loaded \(workouts.count) workouts")
|
|
|
|
let calendar = Calendar.current
|
|
var sortedIntoMonths = [(title: String, workouts: [Workout])]()
|
|
var currentMonth: String? = nil
|
|
var currentWorkouts = [Workout]()
|
|
for workout in workouts.sorted(ascending: false, using: { $0.endDate }) {
|
|
let date = workout.endDate
|
|
let month = calendar.component(.month, from: date)
|
|
let year = calendar.component(.year, from: date)
|
|
let title = "\(calendar.monthSymbols[month-1]) \(year)"
|
|
guard let lastMonth = currentMonth else {
|
|
currentMonth = title
|
|
currentWorkouts = [workout]
|
|
continue
|
|
}
|
|
guard lastMonth == title else {
|
|
sortedIntoMonths.append((lastMonth, currentWorkouts))
|
|
currentMonth = title
|
|
currentWorkouts = [workout]
|
|
continue
|
|
}
|
|
currentWorkouts.append(workout)
|
|
}
|
|
if let currentMonth, !currentWorkouts.isEmpty {
|
|
sortedIntoMonths.append((currentMonth, currentWorkouts))
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.workouts = sortedIntoMonths
|
|
}
|
|
} catch {
|
|
print("Failed to load workouts: \(error)")
|
|
DispatchQueue.main.async {
|
|
self.workouts = []
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadWorkoutTypes(in store: HealthDatabase) {
|
|
do {
|
|
let types = try store.store.workoutTypeFrequencies()
|
|
.sorted(ascending: false) { $0.value }
|
|
.map { (type: $0.key, count: $0.value) }
|
|
DispatchQueue.main.async {
|
|
self.workoutTypeCounts = types
|
|
}
|
|
} catch {
|
|
print("Failed to get workout frequencies: \(error)")
|
|
DispatchQueue.main.async {
|
|
self.workoutTypeCounts = []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
WorkoutTab()
|
|
.environmentObject(Database.empty)
|
|
}
|