Skip to content

Commit

Permalink
Study Home - Step Count Goal & Daily Step Count Display (#7)
Browse files Browse the repository at this point in the history
# 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
PSchmiedmayer authored Mar 25, 2024
1 parent a0facc8 commit ca01a0f
Show file tree
Hide file tree
Showing 53 changed files with 1,486 additions and 303 deletions.
1 change: 0 additions & 1 deletion .github/workflows/beta-deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,5 @@ jobs:
contents: read
with:
arguments: '--debug'
customcommand: 'npm install --prefix functions'
secrets:
GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_BASE64 }}
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
runsonlabels: '["macOS", "self-hosted"]'
setupSimulators: true
setupfirebaseemulator: true
customcommand: "npm install --prefix ./functions && firebase emulators:exec 'fastlane test'"
customcommand: "npm install --prefix ./functions && firebase emulators:exec --import=./firebase 'fastlane test'"
uploadcoveragereport:
name: Upload Coverage Report
needs: buildandtest
Expand Down
6 changes: 6 additions & 0 deletions .reuse/dep5
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.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,31 @@ SPDX-License-Identifier: MIT
-->

# StudyApplication
# Spezi Study Application

This repository contains the StudyApplication.
The StudyApplication is using the [Spezi](https://github.com/StanfordSpezi/Spezi) ecosystem and builds on top of the [Stanford Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication).
This repository contains the Spezi Study Application.

> [!NOTE]
> Do you want to download the Spezi Study Application application? You can download it to your iOS device using [TestFlight](https://testflight.apple.com/join/wlseiMRR)!

## Build & Run The Spezi Study Application

The Spezi Study Application uses the [Spezi](https://github.com/StanfordSpezi/Spezi) ecosystem and is based of the [Stanford Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication).

Please refer to the [Build And Run a Spezi Template Application-based Application](https://spezi.health/SpeziTemplateApplication/documentation/templateapplication/setup) instructions and other documentation for the [Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication) and [Stanford Spezi](https://github.com/StanfordSpezi/Spezi) to learn how to build and run the application using Xcode.


## Firebase Emulator-based Development & Testing

Similar to the [Stanford Spezi Template Application](https://github.com/StanfordSpezi/SpeziTemplateApplication), the Study Application uses Firebase to handle user authentication, data storage, and automation using cloud functions.

The [Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator-suite) allows you to replicate a full cloud stack within your development environment.
To load in the development and testing data to test and build the application locally, you need to start the emulator using
```bash
$ firebase emulators:start --import=./firebase
```
at the root of the Spezi Study Application repository.

> [!NOTE]
> Do you want to learn more about the Stanford Spezi Template Application and how to use, extend, and modify this application? Check out the [Stanford Spezi Template Application documentation](https://stanfordspezi.github.io/SpeziTemplateApplication)
Expand All @@ -21,8 +42,6 @@ The StudyApplication is using the [Spezi](https://github.com/StanfordSpezi/Spezi

Contributions to this project are welcome. Please make sure to read the [contribution guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md) and the [contributor covenant code of conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) first.

This project is based on [ContinuousDelivery Example by Paul Schmiedmayer](https://github.com/PSchmiedmayer/ContinousDelivery) and the [Stanford Biodesign Digital Health Template Application](https://github.com/StanfordBDHG/TemplateApplication) provided using the MIT license.


## License

Expand Down
153 changes: 133 additions & 20 deletions StudyApplication.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordBDHG/ResearchKit",
"state" : {
"revision" : "64512d0a0a5cc3e9d5b3fc5217c54f11d0dc044c",
"version" : "2.2.28"
"revision" : "6b28cdf0d06c3d6e96b5585369968b85deac96e0",
"version" : "2.2.29"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
<CommandLineArguments>
<CommandLineArgument
argument = "--disableFirebase"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
Expand Down
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])
}
}
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()
}
}
Loading

0 comments on commit ca01a0f

Please sign in to comment.