Skip to content

Commit 6573048

Browse files
committed
Improve exercise calorie calculation and fix bugs
1 parent 081442e commit 6573048

10 files changed

Lines changed: 111 additions & 111 deletions

File tree

InfiniLink/Core/Exercise/View Model/ExerciseViewModel.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,21 @@ class ExerciseViewModel: ObservableObject {
2222
var appDidEnterBackgroundDate: Date?
2323

2424
let exercises = [
25-
Exercise(id: "outdoor-run", name: "Outdoor Run", icon: "figure.run", components: [.heart, .steps]),
25+
Exercise(id: "outdoor-run", name: "Outdoor Run", icon: "figure.run", components: [.heart, .steps], pace: .jog),
2626
Exercise(id: "outdoor-cycle", name: "Outdoor Cycle", icon: "figure.outdoor.cycle", components: [.heart]),
27-
Exercise(id: "indoor-run", name: "Indoor Run", icon: "figure.run.treadmill", components: [.heart, .steps]),
27+
Exercise(id: "indoor-run", name: "Indoor Run", icon: "figure.run.treadmill", components: [.heart, .steps], pace: .jog),
2828
Exercise(id: "indoor-cycle", name: "Indoor Cycle", icon: "figure.indoor.cycle", components: [.heart]),
2929
Exercise(id: "strength-training", name: "Strength Training", icon: "figure.strengthtraining.traditional", components: [.heart]),
3030
Exercise(id: "table-tennis", name: "Table Tennis", icon: "figure.table.tennis", components: [.heart]),
3131
Exercise(id: "tennis", name: "Tennis", icon: "figure.tennis", components: [.heart]),
32-
Exercise(id: "soccer", name: "Soccer", icon: "figure.indoor.soccer", components: [.heart, .steps]),
33-
Exercise(id: "basketball", name: "Basketball", icon: "figure.basketball", components: [.heart, .steps]),
34-
Exercise(id: "badminton", name: "Badminton", icon: "figure.badminton", components: [.heart, .steps]),
32+
Exercise(id: "soccer", name: "Soccer", icon: "figure.indoor.soccer", components: [.heart, .steps], pace: .run),
33+
Exercise(id: "basketball", name: "Basketball", icon: "figure.basketball", components: [.heart, .steps], pace: .run),
34+
Exercise(id: "badminton", name: "Badminton", icon: "figure.badminton", components: [.heart, .steps], pace: .run),
3535
Exercise(id: "boxing", name: "Boxing", icon: "figure.boxing", components: [.heart]),
3636
Exercise(id: "skiing", name: "Skiing", icon: "figure.skiing.downhill", components: [.heart]),
3737
Exercise(id: "bowling", name: "Bowling", icon: "figure.bowling", components: [.heart, .steps]),
3838
Exercise(id: "figure.golf", name: "Golf", icon: "figure.golf", components: [.heart, .steps]),
39-
Exercise(id: "hockey", name: "Hockey", icon: "figure.hockey", components: [.heart, .steps])
39+
Exercise(id: "hockey", name: "Hockey", icon: "figure.hockey", components: [.heart, .steps], pace: .fastRun)
4040
]
4141

4242
init() {
@@ -77,15 +77,16 @@ class ExerciseViewModel: ObservableObject {
7777
currentExercise = exercise
7878
}
7979

80-
func saveExercise(_ exercise: String, startDate: Date, heartPoints: [HeartDataPoint], viewContext: NSManagedObjectContext) {
80+
func saveExercise(_ exercise: Exercise, startDate: Date, heartPoints: [HeartDataPoint], viewContext: NSManagedObjectContext) {
8181
let newExercise = UserExercise(context: viewContext)
8282

8383
newExercise.id = UUID()
8484
newExercise.startDate = startDate
8585
newExercise.endDate = Date()
86-
newExercise.exerciseId = exercise
86+
newExercise.exerciseId = exercise.id
8787
newExercise.heartPoints = NSSet(array: heartPoints)
8888
newExercise.steps = Int32(stepsTaken)
89+
newExercise.caloriesBurned = FitnessCalculator().calculateCaloriesBurned(steps: stepsTaken, pace: exercise.pace)
8990

9091
saveContext(viewContext)
9192
}

InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ struct ActiveExerciseView: View {
5151
.foregroundStyle(.blue)
5252
Text(String(exerciseViewModel.stepsTaken))
5353
}
54-
// TODO: add to Core Data
5554
HStack(spacing: 6) {
5655
Image(systemName: "flame.fill")
5756
.foregroundStyle(.orange)
@@ -102,7 +101,7 @@ struct ActiveExerciseView: View {
102101
timer?.invalidate()
103102

104103
if exerciseViewModel.exerciseTime >= 30 {
105-
exerciseViewModel.saveExercise(exercise.id, startDate: Date().addingTimeInterval(-exerciseViewModel.exerciseTime), heartPoints: Array(heartPoints), viewContext: viewContext)
104+
exerciseViewModel.saveExercise(exercise, startDate: Date().addingTimeInterval(-exerciseViewModel.exerciseTime), heartPoints: Array(heartPoints), viewContext: viewContext)
106105
}
107106
} label: {
108107
Text("End Exercise")

InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ struct ExerciseDetailView: View {
8080
}
8181
if exercise().components.contains(.steps) {
8282
Section("Steps") {
83-
let calories = FitnessCalculator().calculateCaloriesBurned(steps: Int(userExercise.steps))
84-
85-
Text("You took \(userExercise.steps) step\(userExercise.steps == 1 ? "" : "s") and burned \(calories > 1 ? String(format: "%.0f", calories) + "calories": "less than one calorie").")
83+
Text("You took \(userExercise.steps) step\(userExercise.steps == 1 ? "" : "s") and burned \(userExercise.caloriesBurned > 1 ? String(format: "%.0f", userExercise.caloriesBurned) + "calories": "less than one calorie").")
8684
}
8785
}
8886
}

InfiniLink/Core/StepsView.swift

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ struct StepsView: View {
1313
@ObservedObject var chartManager = ChartManager.shared
1414
@ObservedObject var personalizationController = PersonalizationController.shared
1515

16-
@FetchRequest var stepCounts: FetchedResults<StepCounts>
17-
1816
@AppStorage("stepChartDataSelection") private var dataSelection = 0
1917

2018
let exerciseCalculator = FitnessCalculator()
@@ -27,14 +25,6 @@ struct StepsView: View {
2725
}
2826
return 0
2927
}
30-
31-
init() {
32-
_stepCounts = FetchRequest(
33-
entity: StepCounts.entity(),
34-
sortDescriptors: [NSSortDescriptor(keyPath: \StepCounts.timestamp, ascending: true)],
35-
predicate: NSPredicate(format: "deviceId == %@", BLEManager.shared.pairedDeviceID ?? "")
36-
)
37-
}
3828

3929
var body: some View {
4030
GeometryReader { geo in
@@ -45,18 +35,17 @@ struct StepsView: View {
4535
let unitsFull = personalizationController.units == .imperial ? "mile" : "kilometer"
4636
let units = personalizationController.units == .imperial ? "mi" : "km"
4737
let distance = exerciseCalculator.calculateDistance(steps: steps())
48-
let stepsPerUnit = exerciseCalculator.stepsPerUnit(steps: steps())
38+
let stepsPerUnit = exerciseCalculator.stepsPerUnit()
4939

5040
DetailHeaderSubItemView(title: "Distance",
5141
value: String(format: "%.2f", distance),
5242
unit: units,
5343
icon: ("ruler", Color.blue))
5444
DetailHeaderSubItemView(title: "Kcal",
55-
value: String(exerciseCalculator.calculateCaloriesBurned(steps: steps())),
45+
value: String(format: "%.1f", exerciseCalculator.calculateCaloriesBurned(steps: steps())),
5646
icon: ("flame", Color.orange))
57-
DetailHeaderSubItemView(title: "Minutes per \(unitsFull)",
58-
value: String(format: "%.1f", exerciseCalculator.minutesForDistance(distance: distance)),
59-
unit: "min\(distance == 1 ? "" : "s")",
47+
DetailHeaderSubItemView(title: "Total time",
48+
value: exerciseCalculator.secondsFormatted(seconds: exerciseCalculator.secondsForDistance(distance: distance)),
6049
icon: ("stopwatch", Color.primary))
6150
DetailHeaderSubItemView(title: "Steps per \(unitsFull)",
6251
value: String(stepsPerUnit),

InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D5040f" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
2+
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D70" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
33
<entity name="BatteryDataPoint" representedClassName="BatteryDataPoint" syncable="YES" codeGenerationType="class">
44
<attribute name="deviceId" optional="YES" attributeType="String"/>
55
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
@@ -64,11 +64,12 @@
6464
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
6565
</entity>
6666
<entity name="UserExercise" representedClassName="UserExercise" syncable="YES" codeGenerationType="class">
67+
<attribute name="caloriesBurned" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
6768
<attribute name="endDate" attributeType="Date" usesScalarValueType="NO"/>
6869
<attribute name="exerciseId" attributeType="String"/>
6970
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
7071
<attribute name="startDate" attributeType="Date" usesScalarValueType="NO"/>
7172
<attribute name="steps" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
7273
<relationship name="heartPoints" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HeartDataPoint" inverseName="exercise" inverseEntity="HeartDataPoint"/>
7374
</entity>
74-
</model>
75+
</model>

InfiniLink/Localizable.xcstrings

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,16 @@
435435
},
436436
"Good morning" : {
437437

438+
},
439+
"Great job, you reached your daily step goal today! You've walked %lf %@ and burned around %lf kcal. Keep it up and let's see how many streak days you can build!" : {
440+
"localizations" : {
441+
"en" : {
442+
"stringUnit" : {
443+
"state" : "new",
444+
"value" : "Great job, you reached your daily step goal today! You've walked %1$lf %2$@ and burned around %3$lf kcal. Keep it up and let's see how many streak days you can build!"
445+
}
446+
}
447+
}
438448
},
439449
"H" : {
440450

@@ -769,9 +779,6 @@
769779
},
770780
"To receive reminder notifications, you'll need to give InfiniLink read access to reminders and events." : {
771781

772-
},
773-
"Today you reached your daily step goal! Keep it up, and let's see how many more days can you reach it..." : {
774-
775782
},
776783
"Total" : {
777784

@@ -865,16 +872,6 @@
865872
}
866873
}
867874
},
868-
"You're %lld steps away your daily step goal! %@" : {
869-
"localizations" : {
870-
"en" : {
871-
"stringUnit" : {
872-
"state" : "new",
873-
"value" : "You're %1$lld steps away your daily step goal! %2$@"
874-
}
875-
}
876-
}
877-
},
878875
"You've reached your steps goal" : {
879876

880877
},

InfiniLink/Model/Exercise.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,13 @@ struct Exercise: Identifiable {
1717
var name: String
1818
var icon: String
1919
let components: [ExerciseComponents]
20+
let pace: Pace
21+
22+
init(id: String, name: String, icon: String, components: [ExerciseComponents], pace: Pace = .avgWalk) {
23+
self.id = id
24+
self.name = name
25+
self.icon = icon
26+
self.components = components
27+
self.pace = pace
28+
}
2029
}

InfiniLink/Utils/FitnessCalculator.swift

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,48 @@
77

88
import Foundation
99

10-
class FitnessCalculator {
11-
enum Pace: Int {
12-
case verySlowWalk = 0
13-
case slowWalk = 1
14-
case avgWalk = 2
15-
case briskWalk = 3
16-
case jog = 4
17-
case run = 5
18-
case fastRun = 6
19-
case veryFastRun = 7
10+
enum Pace: Int {
11+
case verySlowWalk = 0
12+
case slowWalk = 1
13+
case avgWalk = 2
14+
case briskWalk = 3
15+
case jog = 4
16+
case run = 5
17+
case fastRun = 6
18+
case veryFastRun = 7
2019

21-
var metValue: Double {
22-
switch self {
23-
case .verySlowWalk: return 2.0
24-
case .slowWalk: return 2.8
25-
case .avgWalk: return 3.5
26-
case .briskWalk: return 4.3
27-
case .jog: return 7.0
28-
case .run: return 9.8
29-
case .fastRun: return 11.0
30-
case .veryFastRun: return 13.0
31-
}
32-
}
33-
34-
var milesPerHour: Double {
35-
switch self {
36-
case .verySlowWalk: return 1.0
37-
case .slowWalk: return 2.0
38-
case .avgWalk: return 3.0
39-
case .briskWalk: return 4.0
40-
case .jog: return 5.0
41-
case .run: return 6.0
42-
case .fastRun: return 7.5
43-
case .veryFastRun: return 10.0
44-
}
20+
var metValue: Double {
21+
switch self {
22+
case .verySlowWalk: return 2.0
23+
case .slowWalk: return 2.8
24+
case .avgWalk: return 3.5
25+
case .briskWalk: return 4.3
26+
case .jog: return 7.0
27+
case .run: return 9.8
28+
case .fastRun: return 11.0
29+
case .veryFastRun: return 13.0
4530
}
4631
}
4732

33+
var milesPerHour: Double {
34+
switch self {
35+
case .verySlowWalk: return 1.0
36+
case .slowWalk: return 2.0
37+
case .avgWalk: return 3.0
38+
case .briskWalk: return 4.0
39+
case .jog: return 5.0
40+
case .run: return 6.0
41+
case .fastRun: return 7.5
42+
case .veryFastRun: return 10.0
43+
}
44+
}
45+
}
46+
47+
class FitnessCalculator {
4848
let personalizationController = PersonalizationController.shared
4949
let bleManager = BLEManager.shared
5050

51-
func strideLength(pace: FitnessCalculator.Pace = .avgWalk) -> Double {
51+
func strideLength(pace: Pace = .avgWalk) -> Double {
5252
let avgStrideRatio = personalizationController.gender == .male ? 0.415 : 0.413
5353
let calculatedHeight = personalizationController.calculatedHeight
5454
let height = personalizationController.units == .metric ? (calculatedHeight / 2.54) : (calculatedHeight) // Convert to inches
@@ -72,7 +72,7 @@ class FitnessCalculator {
7272
return strideLength
7373
}
7474

75-
func calculateDistance(steps: Int, pace: FitnessCalculator.Pace = .avgWalk) -> Double {
75+
func calculateDistance(steps: Int, pace: Pace = .avgWalk) -> Double {
7676
var distance = strideLength(pace: pace) * Double(steps)
7777

7878
if personalizationController.units == .imperial {
@@ -84,8 +84,7 @@ class FitnessCalculator {
8484
return distance
8585
}
8686

87-
func stepsPerMinute(steps: Int, pace: FitnessCalculator.Pace = .avgWalk) -> Int {
88-
// TODO: update for metric
87+
func stepsPerMinute(steps: Int, pace: Pace = .avgWalk) -> Int {
8988
let distance = calculateDistance(steps: steps, pace: pace)
9089
let timeMinutes = (distance / pace.milesPerHour) * 60.0
9190

@@ -95,7 +94,7 @@ class FitnessCalculator {
9594
return Int(ceil(spm))
9695
}
9796

98-
func calculateCaloriesBurned(steps: Int, pace: FitnessCalculator.Pace = .avgWalk) -> Int {
97+
func calculateCaloriesBurned(steps: Int, pace: Pace = .avgWalk) -> Double {
9998
// TODO: support custom duration
10099
let spm = stepsPerMinute(steps: steps, pace: pace)
101100
let weight = personalizationController.calculatedWeight
@@ -104,17 +103,31 @@ class FitnessCalculator {
104103

105104
guard durationInHours > 0 else { return 0 }
106105

107-
let caloriesBurned = Int(ceil(pace.metValue * calculatedWeight * durationInHours))
108-
return caloriesBurned
106+
return pace.metValue * calculatedWeight * durationInHours
109107
}
110108

111-
func minutesForDistance(distance: Double, pace: FitnessCalculator.Pace = .avgWalk) -> Double {
112-
let speed = personalizationController.units == .imperial ? pace.milesPerHour : (pace.milesPerHour * 1.60934)
113-
return (distance / speed) * 60
114-
}
115-
116-
func stepsPerUnit(steps: Int, pace: FitnessCalculator.Pace = .avgWalk) -> Int {
109+
func stepsPerUnit(pace: Pace = .avgWalk) -> Int {
117110
let unitInInches = personalizationController.units == .imperial ? 63360 : 39370.1
118111
return Int(ceil(unitInInches / strideLength()))
119112
}
113+
114+
func secondsForDistance(distance: Double, pace: Pace = .avgWalk) -> Int {
115+
let speed: Double = personalizationController.units == .imperial ? pace.milesPerHour : (pace.milesPerHour * 1.60934)
116+
117+
return Int(ceil((distance / speed) * 60 * 60))
118+
}
119+
120+
func secondsFormatted(seconds: Int) -> String {
121+
let hours = seconds / 3600
122+
let minutes = (seconds % 3600) / 60
123+
let seconds = seconds % 60
124+
125+
if hours > 0 {
126+
return "\(hours) hr\(hours == 1 ? "" : "s") and \(minutes) min\(minutes == 1 ? "" : "s")"
127+
} else if minutes > 0 {
128+
return "\(minutes) min\(minutes == 1 ? "" : "s")"
129+
} else {
130+
return "<1 min"
131+
}
132+
}
120133
}

InfiniLink/Utils/PersistenceController.swift

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,15 @@ struct PersistenceController {
1616
container = NSPersistentContainer(name: "InfiniLink")
1717
if inMemory {
1818
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
19-
} else {
20-
let description = container.persistentStoreDescriptions.first
21-
description?.shouldMigrateStoreAutomatically = true
22-
description?.shouldInferMappingModelAutomatically = true
2319
}
24-
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
25-
if let error {
26-
/*
27-
Typical reasons for an error here include:
28-
* The parent directory does not exist, cannot be created, or disallows writing.
29-
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
30-
* The device is out of space.
31-
* The store could not be migrated to the current model version.
32-
Check the error message to determine what the actual problem was.
33-
*/
34-
log("Unresolved error loading stores: \(error.localizedDescription)", caller: "PersistenceController")
20+
container.loadPersistentStores { storeDescription, error in
21+
storeDescription.shouldMigrateStoreAutomatically = true
22+
storeDescription.shouldInferMappingModelAutomatically = true
23+
24+
if let error = error as NSError? {
25+
fatalError("Unresolved error \(error), \(error.userInfo)")
3526
}
36-
})
27+
}
3728
container.viewContext.automaticallyMergesChangesFromParent = true
3829
}
3930

0 commit comments

Comments
 (0)