Skip to content

Commit 39e9157

Browse files
committed
Update fitness calculation methods and UX
1 parent 6573048 commit 39e9157

8 files changed

Lines changed: 89 additions & 29 deletions

File tree

InfiniLink/Core/Components/Charts/Steps/StepChartView.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct StepChartView: View {
1919
@ObservedObject var chartManager = ChartManager.shared
2020
@ObservedObject var deviceManager = DeviceManager.shared
2121
@ObservedObject var stepCountManager = StepCountManager.shared
22+
@ObservedObject var personalizationController = PersonalizationController.shared
2223

2324
@AppStorage("stepChartDataSelection") var stepChartDataSelection = 0
2425

@@ -27,6 +28,8 @@ struct StepChartView: View {
2728
@State private var selectedDate = Date()
2829
@State private var selectedSteps = 0
2930

31+
let fitnessCalculator = FitnessCalculator()
32+
3033
func steps() -> [StepChartDataPoint] {
3134
let calendar = Calendar.current
3235
let now = Date()
@@ -142,17 +145,23 @@ struct StepChartView: View {
142145
.listRowBackground(Color.clear)
143146
.listRowInsets(EdgeInsets(top: 18, leading: 0, bottom: 0, trailing: 0))
144147
Section {
148+
let steps = Int(chartManager.stepPoints().last?.steps ?? 0)
149+
145150
if stepCountManager.hasReachedStepGoal {
146-
Text("Today you reached your daily step goal! Keep it up, and let's see how many more days can you reach it...")
151+
Text("Great job, you reached your daily step goal today! You've walked \(fitnessCalculator.calculateDistance(steps: steps)) \(personalizationController.units == .imperial ? "miles" : "kilometers") and burned around \(fitnessCalculator.calculateCaloriesBurned(steps: steps)) kcal.")
147152
} else {
148-
let encouragementString: String = {
149-
if (stepCountManager.stepGoal - bleManager.stepCount) <= 1000 {
150-
return "Take a short walk or a start an activity to complete your goal."
151-
}
152-
return "Complete a few activities to reach your goal."
153-
}()
153+
let stepsRemaining = stepCountManager.stepGoal - steps
154+
let distanceRemaining = fitnessCalculator.calculateDistance(steps: stepsRemaining)
155+
let caloriesRemaining = fitnessCalculator.calculateCaloriesBurned(steps: stepsRemaining)
156+
let timeRemaining = fitnessCalculator.secondsFormatted(seconds: Int(fitnessCalculator.secondsForDistance(distance: distanceRemaining)), full: true)
154157

155-
Text("You're \(stepCountManager.stepGoal - bleManager.stepCount) steps away your daily step goal! \(encouragementString)")
158+
if stepsRemaining <= 1000 {
159+
Text("You're almost there! A quick \(String(format: "%.1f", distanceRemaining)) \(personalizationController.units == .imperial ? "mile" : "km") walk will get you to your goal. It should only take you about \(timeRemaining).")
160+
} else if stepsRemaining <= 2500 {
161+
Text("You're making great progress! You have about \(String(format: "%.1f", distanceRemaining)) \(personalizationController.units == .imperial ? "miles" : "kilometers") to walk. At your current pace, you'll hit your goal in \(timeRemaining).")
162+
} else {
163+
Text("You're \(stepsRemaining) steps away from your goal, which is about \(String(format: "%.1f", distanceRemaining)) \(personalizationController.units == .imperial ? "miles" : "kilometers"). Once you complete your goal, you'll have burned \(caloriesRemaining) kcal and walked for about \(timeRemaining)!")
164+
}
156165
}
157166
}
158167
}

InfiniLink/Core/Components/DetailHeaderView.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ struct DetailHeaderSubItemView: View {
3030
let value: String
3131
let unit: String?
3232
let icon: (String, Color)?
33+
let completion: (() -> Void)?
3334

34-
init(title: String, value: String, unit: String? = nil, icon: (String, Color)? = nil) {
35+
init(title: String, value: String, unit: String? = nil, icon: (String, Color)? = nil, completion: (() -> Void)? = nil) {
3536
self.title = title
3637
self.value = value
3738
self.unit = unit
3839
self.icon = icon
40+
self.completion = completion
3941
}
4042

4143
var body: some View {
@@ -47,9 +49,18 @@ struct DetailHeaderSubItemView: View {
4749
.frame(minWidth: 30)
4850
}
4951
VStack(alignment: .leading, spacing: 3) {
50-
Text(title.uppercased())
51-
.font(.caption)
52-
.foregroundStyle(.gray)
52+
HStack(alignment: .top, spacing: 3) {
53+
Text(title.uppercased())
54+
.font(.caption.weight(.medium))
55+
if let completion {
56+
Image(systemName: "info.circle")
57+
.font(.caption.weight(.medium))
58+
.onTapGesture {
59+
completion()
60+
}
61+
}
62+
}
63+
.foregroundStyle(.gray)
5364
HStack(alignment: .firstTextBaseline, spacing: 2) {
5465
Text(value)
5566
.font(.system(size: 22).weight(.semibold))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class ExerciseViewModel: ObservableObject {
8686
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)
89+
newExercise.caloriesBurned = Int32(FitnessCalculator().calculateCaloriesBurned(steps: stepsTaken, pace: exercise.pace))
9090

9191
saveContext(viewContext)
9292
}

InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct ActiveExerciseView: View {
5454
HStack(spacing: 6) {
5555
Image(systemName: "flame.fill")
5656
.foregroundStyle(.orange)
57-
Text(String(format: "%.1f", fitnessCalculator.calculateCaloriesBurned(steps: exerciseViewModel.stepsTaken)))
57+
Text("\(fitnessCalculator.calculateCaloriesBurned(steps: exerciseViewModel.stepsTaken))")
5858
}
5959
}
6060
}

InfiniLink/Core/StepsView.swift

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

16+
@State private var showInfoAlert = false
17+
1618
@AppStorage("stepChartDataSelection") private var dataSelection = 0
1719

1820
let exerciseCalculator = FitnessCalculator()
@@ -32,7 +34,6 @@ struct StepsView: View {
3234
Section {
3335
DetailHeaderView(Header(title: "\(steps())", subtitle: String(deviceManager.settings.stepsGoal), units: "Steps", icon: "figure.walk", accent: .blue), width: geo.size.width) {
3436
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
35-
let unitsFull = personalizationController.units == .imperial ? "mile" : "kilometer"
3637
let units = personalizationController.units == .imperial ? "mi" : "km"
3738
let distance = exerciseCalculator.calculateDistance(steps: steps())
3839
let stepsPerUnit = exerciseCalculator.stepsPerUnit()
@@ -42,15 +43,16 @@ struct StepsView: View {
4243
unit: units,
4344
icon: ("ruler", Color.blue))
4445
DetailHeaderSubItemView(title: "Kcal",
45-
value: String(format: "%.1f", exerciseCalculator.calculateCaloriesBurned(steps: steps())),
46+
value: "\(exerciseCalculator.calculateCaloriesBurned(steps: steps()))",
4647
icon: ("flame", Color.orange))
4748
DetailHeaderSubItemView(title: "Total time",
4849
value: exerciseCalculator.secondsFormatted(seconds: exerciseCalculator.secondsForDistance(distance: distance)),
4950
icon: ("stopwatch", Color.primary))
50-
DetailHeaderSubItemView(title: "Steps per \(unitsFull)",
51+
DetailHeaderSubItemView(title: "SPM",
5152
value: String(stepsPerUnit),
52-
unit: "step\(stepsPerUnit == 1 ? "" : "s")",
53-
icon: ("shoeprints.fill", Color.blue))
53+
icon: ("shoeprints.fill", Color.blue)) {
54+
showInfoAlert = true
55+
}
5456
}
5557
}
5658
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
@@ -62,6 +64,9 @@ struct StepsView: View {
6264
}
6365
.navigationTitle("Steps")
6466
.navigationBarTitleDisplayMode(.inline)
67+
.alert(isPresented: $showInfoAlert) {
68+
Alert(title: Text("Steps Per Minute"), message: Text("SPM stands for steps per minute and measures how many steps you take for each minute of walking."), dismissButton: .default(Text("OK")))
69+
}
6570
}
6671
}
6772

InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
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"/>
67+
<attribute name="caloriesBurned" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
6868
<attribute name="endDate" attributeType="Date" usesScalarValueType="NO"/>
6969
<attribute name="exerciseId" attributeType="String"/>
7070
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>

InfiniLink/Localizable.xcstrings

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,12 +436,12 @@
436436
"Good morning" : {
437437

438438
},
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!" : {
439+
"Great job, you reached your daily step goal today! You've walked %lf %@ and burned around %lld kcal." : {
440440
"localizations" : {
441441
"en" : {
442442
"stringUnit" : {
443443
"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!"
444+
"value" : "Great job, you reached your daily step goal today! You've walked %1$lf %2$@ and burned around %3$lld kcal."
445445
}
446446
}
447447
}
@@ -709,6 +709,9 @@
709709
},
710710
"Software Version" : {
711711

712+
},
713+
"SPM stands for steps per minute and measures how many steps you take for each minute of walking." : {
714+
712715
},
713716
"Start Pairing" : {
714717

@@ -731,6 +734,9 @@
731734
},
732735
"Steps Goal" : {
733736

737+
},
738+
"Steps Per Minute" : {
739+
734740
},
735741
"Sync step and heart rate data to Apple Health." : {
736742

@@ -872,6 +878,36 @@
872878
}
873879
}
874880
},
881+
"You're %lld steps away from your goal, which is about %@ %@. Once you complete your goal, you'll have burned %lld kcal and walked for about %@!" : {
882+
"localizations" : {
883+
"en" : {
884+
"stringUnit" : {
885+
"state" : "new",
886+
"value" : "You're %1$lld steps away from your goal, which is about %2$@ %3$@. Once you complete your goal, you'll have burned %4$lld kcal and walked for about %5$@!"
887+
}
888+
}
889+
}
890+
},
891+
"You're almost there! A quick %@ %@ walk will get you to your goal. It should only take you about %@." : {
892+
"localizations" : {
893+
"en" : {
894+
"stringUnit" : {
895+
"state" : "new",
896+
"value" : "You're almost there! A quick %1$@ %2$@ walk will get you to your goal. It should only take you about %3$@."
897+
}
898+
}
899+
}
900+
},
901+
"You're making great progress! You have about %@ %@ to walk. At your current pace, you'll hit your goal in %@." : {
902+
"localizations" : {
903+
"en" : {
904+
"stringUnit" : {
905+
"state" : "new",
906+
"value" : "You're making great progress! You have about %1$@ %2$@ to walk. At your current pace, you'll hit your goal in %3$@."
907+
}
908+
}
909+
}
910+
},
875911
"You've reached your steps goal" : {
876912

877913
},

InfiniLink/Utils/FitnessCalculator.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class FitnessCalculator {
9494
return Int(ceil(spm))
9595
}
9696

97-
func calculateCaloriesBurned(steps: Int, pace: Pace = .avgWalk) -> Double {
97+
func calculateCaloriesBurned(steps: Int, pace: Pace = .avgWalk) -> Int {
9898
// TODO: support custom duration
9999
let spm = stepsPerMinute(steps: steps, pace: pace)
100100
let weight = personalizationController.calculatedWeight
@@ -103,7 +103,7 @@ class FitnessCalculator {
103103

104104
guard durationInHours > 0 else { return 0 }
105105

106-
return pace.metValue * calculatedWeight * durationInHours
106+
return Int(ceil(pace.metValue * calculatedWeight * durationInHours))
107107
}
108108

109109
func stepsPerUnit(pace: Pace = .avgWalk) -> Int {
@@ -117,17 +117,16 @@ class FitnessCalculator {
117117
return Int(ceil((distance / speed) * 60 * 60))
118118
}
119119

120-
func secondsFormatted(seconds: Int) -> String {
120+
func secondsFormatted(seconds: Int, full: Bool = false) -> String {
121121
let hours = seconds / 3600
122122
let minutes = (seconds % 3600) / 60
123-
let seconds = seconds % 60
124123

125124
if hours > 0 {
126-
return "\(hours) hr\(hours == 1 ? "" : "s") and \(minutes) min\(minutes == 1 ? "" : "s")"
125+
return "\(hours) \(full ? "hour" : "hr")\(hours == 1 ? "" : "s") and \(minutes) \(full ? "minute" : "min")\(minutes == 1 ? "" : "s")"
127126
} else if minutes > 0 {
128-
return "\(minutes) min\(minutes == 1 ? "" : "s")"
127+
return "\(minutes) \(full ? "minute" : "min")\(minutes == 1 ? "" : "s")"
129128
} else {
130-
return "<1 min"
129+
return "<1 \(full ? "minute" : "min")"
131130
}
132131
}
133132
}

0 commit comments

Comments
 (0)