generated from StanfordSpezi/SpeziTemplateApplication
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Study Home - Step Count Goal & Daily Step Count Display (#7)
# Study Home - Step Count Goal & Daily Step Count Display ## ⚙️ Release Notes - Adds a study dashboard section for all enrolled studies. - Allows a user to define a step count goal & display their current daily steps. ## 📝 Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
- Loading branch information
1 parent
a0facc8
commit ca01a0f
Showing
53 changed files
with
1,486 additions
and
303 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ | ||
|
||
Files: firebase/* | ||
Copyright: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
License: MIT | ||
Comment: All firebase emulator testing data is part of the Stanford Spezi Study Application open source project. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
StudyApplication/Engagements/DailyStepCountGoal/DailyStepCountGoal.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// | ||
// This source file is part of the StudyApplication based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import SwiftUI | ||
|
||
|
||
struct DailyStepCountGoal: View { | ||
enum Constants { | ||
static let stepCountGoalIncrement = 500 | ||
} | ||
|
||
let study: Study | ||
@Environment(DailyStepCountGoalModule.self) var dailyStepCountGoalModule | ||
|
||
|
||
var body: some View { | ||
VStack { | ||
todayStepCountSection | ||
Divider() | ||
goalSection | ||
} | ||
} | ||
|
||
|
||
@ViewBuilder private var todayStepCountSection: some View { | ||
ZStack { | ||
Gauge(progress: Double(dailyStepCountGoalModule.todayStepCount) / Double(dailyStepCountGoalModule.stepCountGoal)) | ||
VStack { | ||
Text("\(dailyStepCountGoalModule.todayStepCount)") | ||
.font(.largeTitle.bold().monospacedDigit()) | ||
.minimumScaleFactor(0.3) | ||
.contentTransition(.numericText()) | ||
.animation(.easeOut, value: dailyStepCountGoalModule.todayStepCount) | ||
Text("Steps Today") | ||
.font(.subheadline) | ||
.foregroundStyle(.secondary) | ||
.multilineTextAlignment(.center) | ||
} | ||
.padding(.horizontal, 20) | ||
} | ||
.frame(minHeight: 140, idealHeight: 160, maxHeight: 200) | ||
.padding() | ||
.padding(.horizontal, 32) | ||
} | ||
|
||
@ViewBuilder private var goalSection: some View { | ||
HStack(alignment: .firstTextBaseline) { | ||
Spacer() | ||
decreaseDailyStepCountGoalButton | ||
VStack { | ||
Text("\(dailyStepCountGoalModule.stepCountGoal)") | ||
.font(.title.bold().monospacedDigit()) | ||
.contentTransition(.numericText()) | ||
.animation(.easeOut, value: dailyStepCountGoalModule.stepCountGoal) | ||
Text("Daily Step Count Goal") | ||
.font(.subheadline) | ||
.foregroundStyle(.secondary) | ||
.multilineTextAlignment(.center) | ||
} | ||
.frame(minWidth: 150) | ||
.accessibilityLabel(Text("Daily Step Count Goal: \(dailyStepCountGoalModule.stepCountGoal)")) | ||
increaseDailyStepCountGoalButton | ||
Spacer() | ||
} | ||
.padding(.top) | ||
} | ||
|
||
@ViewBuilder private var decreaseDailyStepCountGoalButton: some View { | ||
Button( | ||
action: { | ||
guard dailyStepCountGoalModule.stepCountGoal > 3 * Constants.stepCountGoalIncrement else { | ||
dailyStepCountGoalModule.stepCountGoal = 2 * Constants.stepCountGoalIncrement | ||
print("Decrease GUARD ...") | ||
return | ||
} | ||
dailyStepCountGoalModule.stepCountGoal -= Constants.stepCountGoalIncrement | ||
print("Decrease ...") | ||
}, | ||
label: { | ||
Image(systemName: "minus.circle") | ||
.accessibilityLabel(Text("Decrease Step Count Goal by \(Constants.stepCountGoalIncrement)")) | ||
} | ||
) | ||
.font(.title) | ||
.foregroundStyle(Color.accentColor) | ||
.buttonStyle(.borderless) | ||
} | ||
|
||
@ViewBuilder private var increaseDailyStepCountGoalButton: some View { | ||
Button( | ||
action: { | ||
dailyStepCountGoalModule.stepCountGoal += Constants.stepCountGoalIncrement | ||
print("Increase ...") | ||
}, | ||
label: { | ||
Image(systemName: "plus.circle") | ||
.accessibilityLabel(Text("Increase Step Count Goal by \(Constants.stepCountGoalIncrement)")) | ||
} | ||
) | ||
.font(.title) | ||
.foregroundStyle(Color.accentColor) | ||
.buttonStyle(.borderless) | ||
} | ||
} | ||
|
||
|
||
#Preview { | ||
let studyModule = StudyModule() | ||
|
||
|
||
return NavigationStack { | ||
List { | ||
StudyApplicationListCard { | ||
DailyStepCountGoal(study: studyModule.studies[0]) | ||
} | ||
} | ||
.studyApplicationList() | ||
.navigationTitle("Daily Step Goal") | ||
} | ||
.previewWith(standard: StudyApplicationStandard()) { | ||
DailyStepCountGoalModule() | ||
studyModule | ||
} | ||
.task { | ||
try? await studyModule.enrollInStudy(study: studyModule.studies[0]) | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
StudyApplication/Engagements/DailyStepCountGoal/DailyStepCountGoalModule.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// | ||
// This source file is part of the StudyApplication based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import HealthKit | ||
import OSLog | ||
import Spezi | ||
import SpeziHealthKit | ||
import SpeziLocalStorage | ||
|
||
|
||
@Observable | ||
class DailyStepCountGoalModule: Module, EnvironmentAccessible, DefaultInitializable { | ||
private let logger = Logger(subsystem: "edu.stanford.spezi.studyapplication", category: "TodayStepCount") | ||
private let healthStore = HKHealthStore() | ||
|
||
@ObservationIgnored @Dependency var localStorage: LocalStorage | ||
@ObservationIgnored @Dependency var healthKit: HealthKit | ||
|
||
private var queryTask: Task<Void, Error>? | ||
private var dayChangedTask: Task<Void, Never>? | ||
private(set) var todayStepCount: Int = 0 | ||
var stepCountGoal: Int = 10_000 { | ||
didSet { | ||
do { | ||
try localStorage.store(stepCountGoal, storageKey: StorageKeys.dailyStepCountGoal) | ||
} catch { | ||
logger.error("Could not store enrolled studies.") | ||
} | ||
} | ||
} | ||
|
||
|
||
required init() { } | ||
|
||
|
||
func configure() { | ||
do { | ||
if ProcessInfo.processInfo.isPreviewSimulator { | ||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in | ||
self.todayStepCount += Int.random(in: 20...42) | ||
} | ||
} else { | ||
#warning("We need to store the step count goal in Firebase and observe changes in the study app.") | ||
self.stepCountGoal = try localStorage.read(storageKey: StorageKeys.dailyStepCountGoal) | ||
} | ||
} catch { | ||
logger.info("Could not retrieve existing step count goal.") | ||
} | ||
|
||
// We use observation tracking to observe healthKit.authorized. | ||
// If the value is false, the query is not executed and the observation tracking will let us know if the value changed. | ||
withContinousObservation(of: self.healthKit.authorized) { authorized in | ||
if authorized { | ||
self.executeStatisticsQuery() | ||
} | ||
} | ||
} | ||
|
||
private func executeStatisticsQuery() { | ||
guard healthKit.authorized else { | ||
return | ||
} | ||
|
||
if let queryTask { | ||
queryTask.cancel() | ||
} | ||
|
||
observeDayChanged() | ||
|
||
queryTask = Task { | ||
let calendar = Calendar.current | ||
let startOfDay = calendar.startOfDay(for: .now) | ||
|
||
guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else { | ||
logger.debug("Could not generate end of day for day: \(startOfDay). Will not execure query.") | ||
return | ||
} | ||
|
||
let stepsToday = HKSamplePredicate.quantitySample( | ||
type: HKQuantityType(.stepCount), | ||
predicate: HKQuery.predicateForSamples(withStart: startOfDay, end: endOfDay) | ||
) | ||
|
||
let stepsTodayQuery = HKStatisticsCollectionQueryDescriptor( | ||
predicate: stepsToday, | ||
options: .cumulativeSum, | ||
anchorDate: startOfDay, | ||
intervalComponents: DateComponents(day: 1) | ||
) | ||
|
||
do { | ||
for try await results in stepsTodayQuery.results(for: healthStore) { | ||
guard !Task.isCancelled else { | ||
logger.debug("Task was cancelled, exeting results loop.") | ||
return | ||
} | ||
|
||
guard let result = results.statisticsCollection.statistics(for: startOfDay), | ||
let stepCount = result.sumQuantity()?.doubleValue(for: HKUnit.count()) else { | ||
logger.warning("Could not retrieve expected result from startistics collection.") | ||
continue | ||
} | ||
|
||
self.todayStepCount = Int(stepCount) | ||
} | ||
} catch { | ||
logger.error("Could not execure HealthKit query: \(error)") | ||
} | ||
} | ||
} | ||
|
||
private func observeDayChanged() { | ||
guard dayChangedTask == nil else { | ||
return | ||
} | ||
|
||
dayChangedTask = Task { | ||
for await _ in NotificationCenter.default.notifications(named: .NSCalendarDayChanged) { | ||
logger.debug("Day did change.") | ||
executeStatisticsQuery() | ||
} | ||
} | ||
} | ||
|
||
|
||
deinit { | ||
dayChangedTask?.cancel() | ||
queryTask?.cancel() | ||
} | ||
} |
Oops, something went wrong.