From 63decd709221ae7fbeaba60cb7575a1d915172c0 Mon Sep 17 00:00:00 2001 From: krugerk <4656811+krugerk@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:46:12 +0100 Subject: [PATCH] also toothbrushing in sessions per day --- BeeKit/HeathKit/HealthKitConfig.swift | 1 + .../ToothbrushingHealthKitMetric.swift | 36 ++++++++++ ...ToothbrushingSessionsHealthKitMetric.swift | 72 +++++++++++++++++++ .../ToothbrushingHealthKitMetric.swift | 21 ------ BeeSwift.xcodeproj/project.pbxproj | 14 +++- 5 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 BeeKit/HeathKit/Toothbrushing/ToothbrushingHealthKitMetric.swift create mode 100644 BeeKit/HeathKit/Toothbrushing/ToothbrushingSessionsHealthKitMetric.swift delete mode 100644 BeeKit/HeathKit/ToothbrushingHealthKitMetric.swift diff --git a/BeeKit/HeathKit/HealthKitConfig.swift b/BeeKit/HeathKit/HealthKitConfig.swift index 4bfd0ff1..22f674c7 100644 --- a/BeeKit/HeathKit/HealthKitConfig.swift +++ b/BeeKit/HeathKit/HealthKitConfig.swift @@ -53,6 +53,7 @@ public enum HealthKitConfig { // Self care ToothbrushingHealthKitMetric.make(), + ToothbrushingSessionsHealthKitMetric.make(), // Sleep TimeInBedHealthKitMetric(humanText: "Time in bed", databaseString: "timeInBed", category: .Sleep), diff --git a/BeeKit/HeathKit/Toothbrushing/ToothbrushingHealthKitMetric.swift b/BeeKit/HeathKit/Toothbrushing/ToothbrushingHealthKitMetric.swift new file mode 100644 index 00000000..c505babb --- /dev/null +++ b/BeeKit/HeathKit/Toothbrushing/ToothbrushingHealthKitMetric.swift @@ -0,0 +1,36 @@ +import Foundation +import HealthKit + +/// tracks toothbrushing, in number of seconds per day (daystamp) +class ToothbrushingHealthKitMetric: CategoryHealthKitMetric { + private static let healthkitMetric = ["toothbrushing", "seconds-per-day"].joined(separator: "|") + + private init(humanText: String, + databaseString: String, + category: HealthKitCategory) { + super.init(humanText: humanText, + databaseString: databaseString, + category: category, + hkSampleType: HKObjectType.categoryType(forIdentifier: .toothbrushingEvent)!) + } + + override func units(healthStore : HKHealthStore) async throws -> HKUnit { + HKUnit.second() + } + + static func make() -> ToothbrushingHealthKitMetric { + .init(humanText: "Teethbrushing (in seconds per day)", + databaseString: healthkitMetric, + category: HealthKitCategory.SelfCare) + } + + override func recentDataPoints(days: Int, deadline: Int, healthStore: HKHealthStore) async throws -> [any BeeDataPoint] { + try await super.recentDataPoints(days: days, deadline: deadline, healthStore: healthStore) + .map { + NewDataPoint(requestid: $0.requestid, + daystamp: $0.daystamp, + value: $0.value, + comment: "Auto-entered via Apple Health (\(Self.healthkitMetric))") + } + } +} diff --git a/BeeKit/HeathKit/Toothbrushing/ToothbrushingSessionsHealthKitMetric.swift b/BeeKit/HeathKit/Toothbrushing/ToothbrushingSessionsHealthKitMetric.swift new file mode 100644 index 00000000..b04c2493 --- /dev/null +++ b/BeeKit/HeathKit/Toothbrushing/ToothbrushingSessionsHealthKitMetric.swift @@ -0,0 +1,72 @@ +// Part of BeeSwift. Copyright Beeminder + +/// tracks toothbrushing, in number of sessions per day (daystamp) +class ToothbrushingSessionsHealthKitMetric: CategoryHealthKitMetric { + private static let healthkitMetric = ["toothbrushing", "sessions-per-day"].joined(separator: "|") + + private init(humanText: String, + databaseString: String, + category: HealthKitCategory) { + super.init(humanText: humanText, + databaseString: databaseString, + category: category, + hkSampleType: HKObjectType.categoryType(forIdentifier: .toothbrushingEvent)!) + } + + override func units(healthStore : HKHealthStore) async throws -> HKUnit { + .count() + } + + static func make() -> ToothbrushingSessionsHealthKitMetric { + .init(humanText: "Teethbrushing (in sessions per day)", + databaseString: healthkitMetric, + category: HealthKitCategory.SelfCare) + } + + override func recentDataPoints(days: Int, deadline: Int, healthStore: HKHealthStore) async throws -> [any BeeDataPoint] { + let todayDaystamp = Daystamp.now(deadline: deadline) + let startDaystamp = todayDaystamp - days + + let predicate = HKQuery.predicateForSamples(withStart: startDaystamp.start(deadline: deadline), + end: todayDaystamp.end(deadline: deadline)) + + let samples = try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<[HKSample], Error>) in + let query = HKSampleQuery(sampleType: sampleType(), + predicate: predicate, + limit: HKObjectQueryNoLimit, + sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)], + resultsHandler: { (query, samples, error) in + if let error { + continuation.resume(throwing: error) + } else if let samples { + continuation.resume(returning: samples) + } else { + continuation.resume(throwing: HealthKitError("HKSampleQuery did not return samples")) + } + }) + healthStore.execute(query) + }) + .compactMap { $0 as? HKCategorySample } + + let calendar = Calendar.autoupdatingCurrent + let groupedByDay = Dictionary(grouping: samples, by: { sample in + calendar.startOfDay(for: sample.startDate) + }) + + let dailyCounts = groupedByDay + .map { ($0, $1.count) } + .sorted { $0.0 < $1.0 } + + let datapoints = dailyCounts.map({ (date, numberOfEntries) in + let daystamp = Daystamp(fromDate: date, deadline: deadline) + let requestID = "apple-heath-" + daystamp.description + + return NewDataPoint(requestid: requestID, + daystamp: daystamp, + value: NSNumber(value: numberOfEntries), + comment: "Auto-entered via Apple Health (\(Self.healthkitMetric))") + }) + + return datapoints + } +} diff --git a/BeeKit/HeathKit/ToothbrushingHealthKitMetric.swift b/BeeKit/HeathKit/ToothbrushingHealthKitMetric.swift deleted file mode 100644 index 7c73359b..00000000 --- a/BeeKit/HeathKit/ToothbrushingHealthKitMetric.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import HealthKit - -class ToothbrushingHealthKitMetric : CategoryHealthKitMetric { - private init(humanText: String, - databaseString: String, - category: HealthKitCategory) { - super.init(humanText: humanText, databaseString: databaseString, category: category, - hkSampleType: HKObjectType.categoryType(forIdentifier: .toothbrushingEvent)!) - } - - override func units(healthStore : HKHealthStore) async throws -> HKUnit { - HKUnit.second() - } - - static func make() -> ToothbrushingHealthKitMetric { - .init(humanText: "Teethbrushing (in seconds per day)", - databaseString: HKCategoryTypeIdentifier.toothbrushingEvent.rawValue, - category: HealthKitCategory.SelfCare) - } -} diff --git a/BeeSwift.xcodeproj/project.pbxproj b/BeeSwift.xcodeproj/project.pbxproj index ab83032f..5dd25010 100644 --- a/BeeSwift.xcodeproj/project.pbxproj +++ b/BeeSwift.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 9B1DCA5B2D10EA76006A64D9 /* ToothbrushingHealthKitMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1DCA5A2D10EA76006A64D9 /* ToothbrushingHealthKitMetric.swift */; }; + 9B7D44662D12C304003B62B1 /* ToothbrushingSessionsHealthKitMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B7D44652D12C304003B62B1 /* ToothbrushingSessionsHealthKitMetric.swift */; }; 9B8CA57D24B120CA009C86C2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */; }; A10D4E931B07948500A72D29 /* DatapointsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10D4E921B07948500A72D29 /* DatapointsTableView.swift */; }; A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */; }; @@ -219,6 +220,7 @@ /* Begin PBXFileReference section */ 9B1DCA5A2D10EA76006A64D9 /* ToothbrushingHealthKitMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToothbrushingHealthKitMetric.swift; sourceTree = ""; }; + 9B7D44652D12C304003B62B1 /* ToothbrushingSessionsHealthKitMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToothbrushingSessionsHealthKitMetric.swift; sourceTree = ""; }; 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; A10D4E921B07948500A72D29 /* DatapointsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatapointsTableView.swift; sourceTree = ""; }; A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveHKMetricViewController.swift; sourceTree = ""; }; @@ -411,6 +413,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9B7D44642D12C2F3003B62B1 /* Toothbrushing */ = { + isa = PBXGroup; + children = ( + 9B7D44652D12C304003B62B1 /* ToothbrushingSessionsHealthKitMetric.swift */, + 9B1DCA5A2D10EA76006A64D9 /* ToothbrushingHealthKitMetric.swift */, + ); + path = Toothbrushing; + sourceTree = ""; + }; A106AD8B1AF1F62800C434E8 /* Managers */ = { isa = PBXGroup; children = ( @@ -624,7 +635,7 @@ E4E6426E290E27CB004F3EA9 /* HeathKit */ = { isa = PBXGroup; children = ( - 9B1DCA5A2D10EA76006A64D9 /* ToothbrushingHealthKitMetric.swift */, + 9B7D44642D12C2F3003B62B1 /* Toothbrushing */, A1E618FF1E86980900D8ED93 /* HealthKitConfig.swift */, E4E642832910C442004F3EA9 /* CategoryHealthKitMetric.swift */, E4E642872910D055004F3EA9 /* MindfulSessionHealthKitMetric.swift */, @@ -1108,6 +1119,7 @@ E46071012B451FA400305DB4 /* BeeminderModel.xcdatamodeld in Sources */, E458C80D2AD11C64000DCA5C /* Crypto.swift in Sources */, E458C8012AD11BB3000DCA5C /* RequestManager.swift in Sources */, + 9B7D44662D12C304003B62B1 /* ToothbrushingSessionsHealthKitMetric.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };