Add CoreData-based workout tracking app with iOS and watchOS targets
- Migrate from SwiftData to CoreData with CloudKit sync - Add core models: Split, Exercise, Workout, WorkoutLog - Implement tab-based UI: Workout Logs, Splits, Settings - Add SF Symbols picker for split icons - Add exercise picker filtered by split with exclusion of added exercises - Integrate IndieAbout for settings/about section - Add Yams for YAML exercise definition parsing - Include starter exercise libraries (bodyweight, Planet Fitness) - Add Date extensions for formatting (formattedTime, isSameDay) - Format workout date ranges to show time-only for same-day end dates - Add build number update script - Add app icons
This commit is contained in:
54
Workouts Watch App/Models/Exercise.swift
Normal file
54
Workouts Watch App/Models/Exercise.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Exercise)
|
||||
public class Exercise: NSManagedObject, Identifiable {
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var loadType: Int32
|
||||
@NSManaged public var order: Int32
|
||||
@NSManaged public var sets: Int32
|
||||
@NSManaged public var reps: Int32
|
||||
@NSManaged public var weight: Int32
|
||||
@NSManaged public var duration: Date?
|
||||
@NSManaged public var weightLastUpdated: Date?
|
||||
@NSManaged public var weightReminderTimeIntervalWeeks: Int32
|
||||
|
||||
@NSManaged public var split: Split?
|
||||
|
||||
public var id: NSManagedObjectID { objectID }
|
||||
|
||||
var loadTypeEnum: LoadType {
|
||||
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||
set { loadType = Int32(newValue.rawValue) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
extension Exercise {
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Exercise> {
|
||||
return NSFetchRequest<Exercise>(entityName: "Exercise")
|
||||
}
|
||||
|
||||
static func orderedFetchRequest(for split: Split) -> NSFetchRequest<Exercise> {
|
||||
let request = fetchRequest()
|
||||
request.predicate = NSPredicate(format: "split == %@", split)
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Exercise.order, ascending: true)]
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum LoadType: Int, CaseIterable {
|
||||
case none = 0
|
||||
case weight = 1
|
||||
case duration = 2
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .none: "None"
|
||||
case .weight: "Weight"
|
||||
case .duration: "Duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Workouts Watch App/Models/Split.swift
Normal file
66
Workouts Watch App/Models/Split.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
@objc(Split)
|
||||
public class Split: NSManagedObject, Identifiable {
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var color: String
|
||||
@NSManaged public var systemImage: String
|
||||
@NSManaged public var order: Int32
|
||||
|
||||
@NSManaged public var exercises: NSSet?
|
||||
@NSManaged public var workouts: NSSet?
|
||||
|
||||
public var id: NSManagedObjectID { objectID }
|
||||
|
||||
static let unnamed = "Unnamed Split"
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Split {
|
||||
var exercisesArray: [Exercise] {
|
||||
let set = exercises as? Set<Exercise> ?? []
|
||||
return set.sorted { $0.order < $1.order }
|
||||
}
|
||||
|
||||
var workoutsArray: [Workout] {
|
||||
let set = workouts as? Set<Workout> ?? []
|
||||
return set.sorted { $0.start > $1.start }
|
||||
}
|
||||
|
||||
func addToExercises(_ exercise: Exercise) {
|
||||
let items = mutableSetValue(forKey: "exercises")
|
||||
items.add(exercise)
|
||||
}
|
||||
|
||||
func removeFromExercises(_ exercise: Exercise) {
|
||||
let items = mutableSetValue(forKey: "exercises")
|
||||
items.remove(exercise)
|
||||
}
|
||||
|
||||
func addToWorkouts(_ workout: Workout) {
|
||||
let items = mutableSetValue(forKey: "workouts")
|
||||
items.add(workout)
|
||||
}
|
||||
|
||||
func removeFromWorkouts(_ workout: Workout) {
|
||||
let items = mutableSetValue(forKey: "workouts")
|
||||
items.remove(workout)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
extension Split {
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Split> {
|
||||
return NSFetchRequest<Split>(entityName: "Split")
|
||||
}
|
||||
|
||||
static func orderedFetchRequest() -> NSFetchRequest<Split> {
|
||||
let request = fetchRequest()
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Split.order, ascending: true)]
|
||||
return request
|
||||
}
|
||||
}
|
||||
71
Workouts Watch App/Models/Workout.swift
Normal file
71
Workouts Watch App/Models/Workout.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Workout)
|
||||
public class Workout: NSManagedObject, Identifiable {
|
||||
@NSManaged public var start: Date
|
||||
@NSManaged public var end: Date?
|
||||
@NSManaged private var statusRaw: String
|
||||
|
||||
@NSManaged public var split: Split?
|
||||
@NSManaged public var logs: NSSet?
|
||||
|
||||
public var id: NSManagedObjectID { objectID }
|
||||
|
||||
var status: WorkoutStatus {
|
||||
get { WorkoutStatus(rawValue: statusRaw) ?? .notStarted }
|
||||
set { statusRaw = newValue.rawValue }
|
||||
}
|
||||
|
||||
var label: String {
|
||||
if status == .completed, let endDate = end {
|
||||
return "\(start.formattedDate())—\(endDate.formattedDate())"
|
||||
} else {
|
||||
return start.formattedDate()
|
||||
}
|
||||
}
|
||||
|
||||
var statusName: String {
|
||||
return status.displayName
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Workout {
|
||||
var logsArray: [WorkoutLog] {
|
||||
let set = logs as? Set<WorkoutLog> ?? []
|
||||
return set.sorted { $0.order < $1.order }
|
||||
}
|
||||
|
||||
func addToLogs(_ log: WorkoutLog) {
|
||||
let items = mutableSetValue(forKey: "logs")
|
||||
items.add(log)
|
||||
}
|
||||
|
||||
func removeFromLogs(_ log: WorkoutLog) {
|
||||
let items = mutableSetValue(forKey: "logs")
|
||||
items.remove(log)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
extension Workout {
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Workout> {
|
||||
return NSFetchRequest<Workout>(entityName: "Workout")
|
||||
}
|
||||
|
||||
static func recentFetchRequest() -> NSFetchRequest<Workout> {
|
||||
let request = fetchRequest()
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Workout.start, ascending: false)]
|
||||
return request
|
||||
}
|
||||
|
||||
static func fetchRequest(for split: Split) -> NSFetchRequest<Workout> {
|
||||
let request = fetchRequest()
|
||||
request.predicate = NSPredicate(format: "split == %@", split)
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \Workout.start, ascending: false)]
|
||||
return request
|
||||
}
|
||||
}
|
||||
43
Workouts Watch App/Models/WorkoutLog.swift
Normal file
43
Workouts Watch App/Models/WorkoutLog.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(WorkoutLog)
|
||||
public class WorkoutLog: NSManagedObject, Identifiable {
|
||||
@NSManaged public var date: Date
|
||||
@NSManaged public var sets: Int32
|
||||
@NSManaged public var reps: Int32
|
||||
@NSManaged public var weight: Int32
|
||||
@NSManaged private var statusRaw: String?
|
||||
@NSManaged public var order: Int32
|
||||
@NSManaged public var exerciseName: String
|
||||
@NSManaged public var currentStateIndex: Int32
|
||||
@NSManaged public var elapsedSeconds: Int32
|
||||
@NSManaged public var completed: Bool
|
||||
|
||||
@NSManaged public var workout: Workout?
|
||||
|
||||
public var id: NSManagedObjectID { objectID }
|
||||
|
||||
var status: WorkoutStatus? {
|
||||
get {
|
||||
guard let raw = statusRaw else { return nil }
|
||||
return WorkoutStatus(rawValue: raw)
|
||||
}
|
||||
set { statusRaw = newValue?.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
extension WorkoutLog {
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<WorkoutLog> {
|
||||
return NSFetchRequest<WorkoutLog>(entityName: "WorkoutLog")
|
||||
}
|
||||
|
||||
static func orderedFetchRequest(for workout: Workout) -> NSFetchRequest<WorkoutLog> {
|
||||
let request = fetchRequest()
|
||||
request.predicate = NSPredicate(format: "workout == %@", workout)
|
||||
request.sortDescriptors = [NSSortDescriptor(keyPath: \WorkoutLog.order, ascending: true)]
|
||||
return request
|
||||
}
|
||||
}
|
||||
23
Workouts Watch App/Models/WorkoutStatus.swift
Normal file
23
Workouts Watch App/Models/WorkoutStatus.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
enum WorkoutStatus: String, CaseIterable, Codable {
|
||||
case notStarted = "notStarted"
|
||||
case inProgress = "inProgress"
|
||||
case completed = "completed"
|
||||
case skipped = "skipped"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .notStarted:
|
||||
return "Not Started"
|
||||
case .inProgress:
|
||||
return "In Progress"
|
||||
case .completed:
|
||||
return "Completed"
|
||||
case .skipped:
|
||||
return "Skipped"
|
||||
}
|
||||
}
|
||||
|
||||
var name: String { displayName }
|
||||
}
|
||||
Reference in New Issue
Block a user