From d99d83a085b7d6c3fccfd9b5dec891c7ec604357 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Fri, 8 Mar 2024 17:42:55 +0100 Subject: [PATCH] Use new database framework --- HealthImport.xcodeproj/project.pbxproj | 194 +++-------- .../xcshareddata/swiftpm/Package.resolved | 21 +- .../API/HKDatabaseFile+Interface.swift | 68 ---- .../API/HKHealthStore+Interface.swift | 14 - HealthImport/API/HKHealthStoreInterface.swift | 206 ----------- HealthImport/ActivityDetailView.swift | 78 +++-- HealthImport/ActivitySamplesView.swift | 13 +- HealthImport/ContentView.swift | 51 ++- HealthImport/HealthDatabase.swift | 121 ------- HealthImport/HealthImport.entitlements | 10 + HealthImport/HealthImportApp.swift | 10 +- HealthImport/LocationSampleDetailView.swift | 3 +- HealthImport/LocationSampleListView.swift | 5 +- HealthImport/Model/EventMetadata.pb.swift | 208 ----------- HealthImport/Model/Goal.swift | 46 --- .../Model/HKWorkoutActivity+Comparable.swift | 19 - .../Model/HKWorkoutEvent+Identifiable.swift | 10 - HealthImport/Model/MetadataKey.swift | 329 ------------------ HealthImport/Model/MetadataValue.swift | 59 ---- HealthImport/Model/Sample.swift | 146 -------- .../Model/Tables/DataProvenancesTable.swift | 266 -------------- .../Model/Tables/DataSeriesTable.swift | 13 - .../Tables/LocationSeriesDataTable.swift | 98 ------ .../Model/Tables/MetadataKeysTable.swift | 44 --- .../Model/Tables/MetadataTables.swift | 42 --- .../Model/Tables/MetadataValuesTable.swift | 126 ------- HealthImport/Model/Tables/ObjectsTable.swift | 37 -- .../Model/Tables/QuantitySamplesTable.swift | 32 -- HealthImport/Model/Tables/SamplesTable.swift | 96 ----- .../Model/Tables/UnitStringsTable.swift | 27 -- .../Model/Tables/WorkoutActivitiesTable.swift | 119 ------- .../Model/Tables/WorkoutEventsTable.swift | 174 --------- HealthImport/Model/Tables/WorkoutsTable.swift | 102 ------ HealthImport/Model/Workout.swift | 80 ----- .../Preview Content/HealthDatabase+Mock.swift | 1 + .../Preview Content/Location+Mock.swift | 4 +- .../Preview Content/Metadata+Mock.swift | 55 +-- .../Preview Content/Workout+Mock.swift | 2 + .../Preview Content/WorkoutEvent+Mock.swift | 31 +- HealthImport/SampleListView.swift | 11 +- .../HKWorkoutActivityType+Extensions.swift | 187 ---------- .../HKWorkoutEventType+Extensions.swift | 20 -- ...orkoutSessionLocationType+Extensions.swift | 18 - ...rkoutSwimmingLocationType+Extensions.swift | 18 - HealthImport/Support/Workout+Extensions.swift | 36 ++ HealthImport/WorkoutDetailView.swift | 21 +- 46 files changed, 303 insertions(+), 2968 deletions(-) delete mode 100644 HealthImport/API/HKDatabaseFile+Interface.swift delete mode 100644 HealthImport/API/HKHealthStore+Interface.swift delete mode 100644 HealthImport/API/HKHealthStoreInterface.swift delete mode 100644 HealthImport/HealthDatabase.swift create mode 100644 HealthImport/HealthImport.entitlements delete mode 100644 HealthImport/Model/EventMetadata.pb.swift delete mode 100644 HealthImport/Model/Goal.swift delete mode 100644 HealthImport/Model/HKWorkoutActivity+Comparable.swift delete mode 100644 HealthImport/Model/HKWorkoutEvent+Identifiable.swift delete mode 100644 HealthImport/Model/MetadataKey.swift delete mode 100644 HealthImport/Model/MetadataValue.swift delete mode 100644 HealthImport/Model/Sample.swift delete mode 100644 HealthImport/Model/Tables/DataProvenancesTable.swift delete mode 100644 HealthImport/Model/Tables/DataSeriesTable.swift delete mode 100644 HealthImport/Model/Tables/LocationSeriesDataTable.swift delete mode 100644 HealthImport/Model/Tables/MetadataKeysTable.swift delete mode 100644 HealthImport/Model/Tables/MetadataTables.swift delete mode 100644 HealthImport/Model/Tables/MetadataValuesTable.swift delete mode 100644 HealthImport/Model/Tables/ObjectsTable.swift delete mode 100644 HealthImport/Model/Tables/QuantitySamplesTable.swift delete mode 100644 HealthImport/Model/Tables/SamplesTable.swift delete mode 100644 HealthImport/Model/Tables/UnitStringsTable.swift delete mode 100644 HealthImport/Model/Tables/WorkoutActivitiesTable.swift delete mode 100644 HealthImport/Model/Tables/WorkoutEventsTable.swift delete mode 100644 HealthImport/Model/Tables/WorkoutsTable.swift delete mode 100644 HealthImport/Model/Workout.swift delete mode 100644 HealthImport/Support/HKWorkoutActivityType+Extensions.swift delete mode 100644 HealthImport/Support/HKWorkoutEventType+Extensions.swift delete mode 100644 HealthImport/Support/HKWorkoutSessionLocationType+Extensions.swift delete mode 100644 HealthImport/Support/HKWorkoutSwimmingLocationType+Extensions.swift create mode 100644 HealthImport/Support/Workout+Extensions.swift diff --git a/HealthImport.xcodeproj/project.pbxproj b/HealthImport.xcodeproj/project.pbxproj index e5192c5..5256b92 100644 --- a/HealthImport.xcodeproj/project.pbxproj +++ b/HealthImport.xcodeproj/project.pbxproj @@ -12,60 +12,35 @@ 8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; }; 885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */; }; 8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */; }; - 885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002702B5C299900E7D4DB /* HealthDatabase.swift */; }; 885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; }; 885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; }; - 8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */; }; - 8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027E2B5C36A700E7D4DB /* Workout.swift */; }; - 885002852B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */; }; - 885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */; }; 8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */; }; 8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */; }; - 885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */; }; 885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002922B5D129300E7D4DB /* ActivityDetailView.swift */; }; 885002952B5D147100E7D4DB /* DetailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002942B5D147100E7D4DB /* DetailRow.swift */; }; - 885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */; }; - 885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */; }; 8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */; }; 8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029C2B5D197300E7D4DB /* EventDetailView.swift */; }; - 8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */; }; - 885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002A22B5D217600E7D4DB /* MetadataValue.swift */; }; 885002A62B5D296700E7D4DB /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A52B5D296700E7D4DB /* Collections */; }; 885002A82B5D296700E7D4DB /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A72B5D296700E7D4DB /* DequeModule */; }; 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A92B5D296700E7D4DB /* OrderedCollections */; }; E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */; }; E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC742B626B19005B83D3 /* Metadata+Mock.swift */; }; - E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; }; - E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKeysTable.swift */; }; - E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */; }; - E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* SamplesTable.swift */; }; E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; }; - E201EC812B631708005B83D3 /* Goal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC802B631708005B83D3 /* Goal.swift */; }; - E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */; }; + E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E20881D22B76912000D41D95 /* HealthKitExtensions */; }; + E20881D52B76944A00D41D95 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20881D42B76944A00D41D95 /* Test.swift */; }; E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; }; E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; }; E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */; }; E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6832B5E76A4003A8873 /* Location+Mock.swift */; }; - E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6852B5FBF0B003A8873 /* Sample.swift */; }; - E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */; }; - E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */; }; E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; }; - E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */; }; - E27BC6902B5FCEA4003A8873 /* WorkoutActivitiesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */; }; E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */; }; E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6932B5FD587003A8873 /* Workout+Mock.swift */; }; E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */; }; E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; }; - E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */; }; - E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */; }; - E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */; }; + E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA02B99FFDD00BAD02E /* HKDatabase */; }; + E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; }; E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; }; - E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */; }; - E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */; }; - E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */; }; E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; }; - E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */; }; - E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -75,55 +50,29 @@ 8850025E2B5C273E00E7D4DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthdb_secure.sqlite; sourceTree = ""; }; - 885002702B5C299900E7D4DB /* HealthDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthDatabase.swift; sourceTree = ""; }; 885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = ""; }; - 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutsTable.swift; sourceTree = ""; }; - 8850027E2B5C36A700E7D4DB /* Workout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workout.swift; sourceTree = ""; }; - 885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEvent+Identifiable.swift"; sourceTree = ""; }; - 885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivityType+Extensions.swift"; sourceTree = ""; }; 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = ""; }; 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; - 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEventType+Extensions.swift"; sourceTree = ""; }; 885002922B5D129300E7D4DB /* ActivityDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityDetailView.swift; sourceTree = ""; }; 885002942B5D147100E7D4DB /* DetailRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRow.swift; sourceTree = ""; }; - 885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSessionLocationType+Extensions.swift"; sourceTree = ""; }; - 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSwimmingLocationType+Extensions.swift"; sourceTree = ""; }; 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; 8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = ""; }; - 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValuesTable.swift; sourceTree = ""; }; - 885002A22B5D217600E7D4DB /* MetadataValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValue.swift; sourceTree = ""; }; E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+Mock.swift"; sourceTree = ""; }; E201EC742B626B19005B83D3 /* Metadata+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Metadata+Mock.swift"; sourceTree = ""; }; - E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = ""; }; - E201EC782B627572005B83D3 /* MetadataKeysTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKeysTable.swift; sourceTree = ""; }; - E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataTables.swift; sourceTree = ""; }; - E201EC7C2B62930E005B83D3 /* SamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTable.swift; sourceTree = ""; }; E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = ""; }; - E201EC802B631708005B83D3 /* Goal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Goal.swift; sourceTree = ""; }; - E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSeriesDataTable.swift; sourceTree = ""; }; + E20881D42B76944A00D41D95 /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = ""; }; E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = ""; }; E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleDetailView.swift; sourceTree = ""; }; E27BC6832B5E76A4003A8873 /* Location+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+Mock.swift"; sourceTree = ""; }; - E27BC6852B5FBF0B003A8873 /* Sample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sample.swift; sourceTree = ""; }; - E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySamplesTable.swift; sourceTree = ""; }; - E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStringsTable.swift; sourceTree = ""; }; E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesView.swift; sourceTree = ""; }; - E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEventsTable.swift; sourceTree = ""; }; - E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivitiesTable.swift; sourceTree = ""; }; E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HealthDatabase+Mock.swift"; sourceTree = ""; }; E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = ""; }; E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = ""; }; E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; - E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKHealthStoreInterface.swift; sourceTree = ""; }; - E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKHealthStore+Interface.swift"; sourceTree = ""; }; - E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKDatabaseFile+Interface.swift"; sourceTree = ""; }; - E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMetadata.pb.swift; sourceTree = ""; }; - E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectsTable.swift; sourceTree = ""; }; - E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvenancesTable.swift; sourceTree = ""; }; + E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = ""; }; E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; - E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivity+Comparable.swift"; sourceTree = ""; }; - E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSeriesTable.swift; sourceTree = ""; }; + E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -132,6 +81,8 @@ buildActionMask = 2147483647; files = ( 885002A62B5D296700E7D4DB /* Collections in Frameworks */, + E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */, + E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */, 885002772B5C2FC400E7D4DB /* SQLite in Frameworks */, 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */, 885002A82B5D296700E7D4DB /* DequeModule in Frameworks */, @@ -161,6 +112,7 @@ 885002592B5C273C00E7D4DB /* HealthImport */ = { isa = PBXGroup; children = ( + E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */, 8850026A2B5C276B00E7D4DB /* Resources */, 8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */, 8850025C2B5C273C00E7D4DB /* ContentView.swift */, @@ -174,9 +126,7 @@ 885002942B5D147100E7D4DB /* DetailRow.swift */, 8850025E2B5C273E00E7D4DB /* Assets.xcassets */, 885002602B5C273E00E7D4DB /* Preview Content */, - 885002702B5C299900E7D4DB /* HealthDatabase.swift */, - E2FDFF1D2B6BD1F00080A7B3 /* API */, - 885002812B5C37B700E7D4DB /* Model */, + E20881D42B76944A00D41D95 /* Test.swift */, 885002832B5C37C600E7D4DB /* Support */, ); path = HealthImport; @@ -204,69 +154,20 @@ path = Resources; sourceTree = ""; }; - 885002812B5C37B700E7D4DB /* Model */ = { - isa = PBXGroup; - children = ( - E2FDFF232B6C509D0080A7B3 /* Tables */, - E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */, - E201EC802B631708005B83D3 /* Goal.swift */, - E201EC762B626FC1005B83D3 /* MetadataKey.swift */, - 885002A22B5D217600E7D4DB /* MetadataValue.swift */, - E27BC6852B5FBF0B003A8873 /* Sample.swift */, - 8850027E2B5C36A700E7D4DB /* Workout.swift */, - E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */, - 885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */, - ); - path = Model; - sourceTree = ""; - }; 885002832B5C37C600E7D4DB /* Support */ = { isa = PBXGroup; children = ( E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */, 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */, 885002782B5C320400E7D4DB /* Optional+Extensions.swift */, - 885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */, - 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */, - 885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */, - 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */, 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */, E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */, E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */, + E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */, ); path = Support; sourceTree = ""; }; - E2FDFF1D2B6BD1F00080A7B3 /* API */ = { - isa = PBXGroup; - children = ( - E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */, - E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */, - E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */, - ); - path = API; - sourceTree = ""; - }; - E2FDFF232B6C509D0080A7B3 /* Tables */ = { - isa = PBXGroup; - children = ( - E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */, - E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */, - E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */, - E201EC782B627572005B83D3 /* MetadataKeysTable.swift */, - E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */, - 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */, - E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */, - E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */, - E201EC7C2B62930E005B83D3 /* SamplesTable.swift */, - E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */, - E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */, - E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */, - 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */, - ); - path = Tables; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -289,6 +190,8 @@ 885002A72B5D296700E7D4DB /* DequeModule */, 885002A92B5D296700E7D4DB /* OrderedCollections */, E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */, + E20881D22B76912000D41D95 /* HealthKitExtensions */, + E2A38EA02B99FFDD00BAD02E /* HKDatabase */, ); productName = HealthImport; productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */; @@ -322,6 +225,8 @@ 885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */, 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */, E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */, + E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */, + E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */, ); productRefGroup = 885002582B5C273C00E7D4DB /* Products */; projectDirPath = ""; @@ -351,55 +256,28 @@ buildActionMask = 2147483647; files = ( E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */, + E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */, E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */, - 885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */, 8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */, 8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */, - 885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */, 885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */, - E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */, - E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */, - 885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */, E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */, E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */, 8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */, - 885002852B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift in Sources */, - 885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */, - E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */, E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */, - E27BC6902B5FCEA4003A8873 /* WorkoutActivitiesTable.swift in Sources */, - E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */, - 8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */, - E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */, - 885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */, E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */, 885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */, - 8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */, - E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */, - E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */, - E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */, - 8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */, E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */, E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */, 8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */, - E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */, - 885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */, - E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */, - E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */, 885002952B5D147100E7D4DB /* DetailRow.swift in Sources */, E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */, E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */, - E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */, - E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */, 8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */, - E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */, - E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */, E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */, + E20881D52B76944A00D41D95 /* Test.swift in Sources */, E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */, - E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */, - E201EC812B631708005B83D3 /* Goal.swift in Sources */, E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */, - E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */, 8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -459,7 +337,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -516,7 +394,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -531,12 +409,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HealthImport/HealthImport.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"HealthImport/Preview Content\""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHealthShareUsageDescription = "Manage all the health data your choose."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Manage all the health data your choose."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -560,12 +441,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = HealthImport/HealthImport.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"HealthImport/Preview Content\""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHealthShareUsageDescription = "Manage all the health data your choose."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Manage all the health data your choose."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -624,6 +508,22 @@ minimumVersion = 1.0.6; }; }; + E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/christophhagen/HealthKitExtensions"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.0; + }; + }; + E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/christophhagen/iOSHealthDBInterface"; + requirement = { + branch = main; + kind = branch; + }; + }; E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -655,6 +555,16 @@ package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; + E20881D22B76912000D41D95 /* HealthKitExtensions */ = { + isa = XCSwiftPackageProductDependency; + package = E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */; + productName = HealthKitExtensions; + }; + E2A38EA02B99FFDD00BAD02E /* HKDatabase */ = { + isa = XCSwiftPackageProductDependency; + package = E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */; + productName = HKDatabase; + }; E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */; diff --git a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 61050a7..e509030 100644 --- a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,24 @@ { + "originHash" : "d0067804897f78a2b51cdf96069649aae9f635cdf94333101762cb0c84fd39ae", "pins" : [ + { + "identity" : "healthkitextensions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/christophhagen/HealthKitExtensions", + "state" : { + "revision" : "88625ad3480ceea974e47f61e1abb05a84896c73", + "version" : "0.3.0" + } + }, + { + "identity" : "ioshealthdbinterface", + "kind" : "remoteSourceControl", + "location" : "https://github.com/christophhagen/iOSHealthDBInterface", + "state" : { + "branch" : "main", + "revision" : "d638c367f3bbdbffefdf0a4dde8fc0afd73bbaad" + } + }, { "identity" : "sqlite.swift", "kind" : "remoteSourceControl", @@ -28,5 +47,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/HealthImport/API/HKDatabaseFile+Interface.swift b/HealthImport/API/HKDatabaseFile+Interface.swift deleted file mode 100644 index f88262f..0000000 --- a/HealthImport/API/HKDatabaseFile+Interface.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import HealthKit - -struct HKNotSupportedError: Error { - -} - -extension HealthDatabase { - - public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery { - let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler) - - switch sampleType { - case is HKWorkoutType: - workouts(predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler) - return query - case is HKCorrelationType: - // TODO: Implement - break - case is HKQuantityType: - // TODO: Implement - break - case is HKAudiogramSampleType: - // TODO: Implement - break - case is HKElectrocardiogramType: - // TODO: Implement - break - case is HKPrescriptionType: - // TODO: Implement - break - case is HKClinicalType: - // TODO: Implement - break - case is HKCategoryType: - // TODO: Implement - break - case is HKDocumentType: - // TODO: Implement - break - case is HKSeriesType: - // TODO: Implement - break - //case is HKCharacteristicType: - // break - //case is HKActivitySummaryType: - // break - default: - break - } - - resultsHandler(query, nil, HKNotSupportedError()) - return query - - //sampleType.isMinimumDurationRestricted - //sampleType.isMaximumDurationRestricted - //sampleType.maximumAllowedDuration - //sampleType.allowsRecalibrationForEstimates - } - - private func workouts(predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) { - #warning("Get workouts and filter them") - - // Example predicates - // HKQuery.predicateForWorkouts(with: .greaterThanOrEqualTo, duration: 1) - // HKQuery.predicateForWorkouts(with: HKWorkoutActivityType) - } -} diff --git a/HealthImport/API/HKHealthStore+Interface.swift b/HealthImport/API/HKHealthStore+Interface.swift deleted file mode 100644 index 87e306b..0000000 --- a/HealthImport/API/HKHealthStore+Interface.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import HealthKit - -extension HKHealthStore: HKHealthStoreInterface { - - /** - Creates a sample query and executes it. - */ - public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery { - let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler) - execute(query) - return query - } -} diff --git a/HealthImport/API/HKHealthStoreInterface.swift b/HealthImport/API/HKHealthStoreInterface.swift deleted file mode 100644 index c30998f..0000000 --- a/HealthImport/API/HKHealthStoreInterface.swift +++ /dev/null @@ -1,206 +0,0 @@ -import Foundation -import HealthKit -import UIKit - -public protocol HKHealthStoreInterface { - - // MARK: - Accessing HealthKit - - /** - Returns the app’s authorization status for sharing the specified data type. - - This method checks the authorization status for saving data. - - To help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data. - If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store. - If your app is given share permission but not read permission, you see only the data that your app has written to the store. - Data from other sources remains hidden. - - - Parameter type: The type of data. This can be any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType`` , ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType`` or ``HKCorrelationType`` classes). - - Returns: A value indicating the app’s authorization status for this type. For a list of possible values, see `HKAuthorizationStatus`. - */ - func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus - - /** - Indicates whether the system presents the user with a permission sheet if your app requests authorization for the provided types. - - When working with clinical types, users may need to reauthorize access when new data is added. - - - Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration: - ``` - func statusForAuthorizationRequest(toShare typesToShare: Set, read typesToRead: Set) async throws -> HKAuthorizationRequestStatus - ``` - For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously). - - */ - func getRequestStatusForAuthorization( - toShare typesToShare: Set, - read typesToRead: Set, - completion: @escaping (HKAuthorizationRequestStatus, Error?) -> Void - ) - - /** - Returns a Boolean value that indicates whether HealthKit is available on this device. - - By default, HealthKit data is available on iOS and watchOS. HealthKit data is also available on iPadOS 17 or later. However, devices running in an enterprise environment may restrict access to HealthKit data. - - Additionally, while the HealthKit framework is available on iPadOS 16 and earlier and on MacOS 13 and later, these devices don’t have a copy of the HealthKit store. This means you can include HealthKit code in apps running on these devices, simplifying the creation of multiplatform apps. However, they can’t read or write HealthKit data, and calls to ``isHealthDataAvailable()`` return ``false``. - - - Returns: `true` if HealthKit is available; otherwise, `false`. - */ - static func isHealthDataAvailable() -> Bool - - /** - Returns a Boolean value that indicates whether the current device supports clinical records. - - This method returns true if the device is set to a locale where clinical records are supported, or if the user already has clinical records downloaded to their HealthKit store. Otherwise, it returns false. - - This method lets users switch their locale without losing their health records. - */ - func supportsHealthRecords() -> Bool - - /** - Requests permission to save and read the specified data types. - - HealthKit performs these requests asynchronously. If you call this method with a new data type (a type of data that the user hasn’t previously granted or denied permission for in this app), the system automatically displays the permission form, listing all the requested permissions. After the user has finished responding, this method calls its completion block on a background queue. If the user has already chosen to grant or prohibit access to all of the types specified, HealthKit calls the completion without prompting the user. - - - Important: In watchOS 6 and later, this method displays the permission form on Apple Watch, enabling independent HealthKit apps. In watchOS 5 and earlier, this method prompts the user to authorize the app on their paired iPhone. For more information, see Creating Independent watchOS Apps. - - Each data type has two separate permissions, one to read it and one to share it. You can make a single request, and include all the data types your app needs. - - Customize the messages displayed on the permissions sheet by setting the following keys: - * ``NSHealthShareUsageDescription`` customizes the message for reading data. - * ``NSHealthUpdateUsageDescription`` customizes the message for writing data. - - - Warning: You must set the usage keys, or your app will crash when you request authorization. - - For projects created using Xcode 13 or later, set these keys in the Target Properties list on the app’s Info tab. For projects created with Xcode 12 or earlier, set these keys in the apps Info.plist file. For more information, see [Information Property List](doc://com.apple.documentation/documentation/bundleresources/information_property_list). - - After users have set the permissions for your app, they can always change them using either the Settings or the Health app. Your app appears in the Health app’s Sources tab, even if the user didn’t allow permission to read or share data. - - - Parameter typesToShare: A set containing the data types you want to share. This set can contain any concrete subclass of the ``HKSampleType`` class (any of the ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes ). If the user grants permission, your app can create and save these data types to the HealthKit store. - - Parameter typesToRead: A set containing the data types you want to read. This set can contain any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType``, ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes). If the user grants permission, your app can read these data types from the HealthKit store. - - Parameter completion: A block called after the user finishes responding to the request. The system calls this block with the following parameters: - - Parameter success: A Boolean value that indicates whether the request succeeded. This value doesn’t indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, it’s true. - - Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, it’s set to nil. - */ - func requestAuthorization( - toShare typesToShare: Set?, - read typesToRead: Set?, - completion: @escaping (Bool, Error?) -> Void - ) - - /** - Asynchronously requests permission to read a data type that requires per-object authorization (such as vision prescriptions). - - - Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration: - ``` - func requestPerObjectReadAuthorization(for objectType: HKObjectType, predicate: NSPredicate?) async throws - ``` - For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously). - - Some samples require per-object authorization. For these samples, people can select which ones your app can read on a sample-by-sample basis. By default, your app can read any of the per-object authorization samples that it has saved to the HealthKit store; however, you may not always have access to those samples. People can update the authorization status for any of these samples at any time. - - Your app can begin by querying for any samples that it already has permission to read. - - ``` - // Read the newest prescription from the HealthKit store. - let queryDescriptor = HKSampleQueryDescriptor( - predicates: [.visionPrescription()], - sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)], - limit: 1) - - let prescription: HKVisionPrescription - - do { - guard let result = try await queryDescriptor.result(for: store).first else { - print("*** No prescription found. ***") - return - } - prescription = result - } catch { - // Handle the error here. - fatalError("*** An error occurred while reading the most recent vision prescriptions: \(error.localizedDescription) ***") - } - ``` - - Based on the results, you can then decide whether you need to request authorization for additional samples. Call `requestPerObjectReadAuthorization(for:predicate:completion:)` to prompt someone to modify the samples your app has access to read. - - ``` - // Request authorization to read vision prescriptions. - do { - try await store.requestPerObjectReadAuthorization(for: .visionPrescriptionType(), predicate: nil) - } catch HKError.errorUserCanceled { - // Handle the user canceling the authorization request. - print("*** The user canceled the authorization request. ***") - return - } catch { - // Handle the error here. - fatalError("*** An error occurred while requesting permission to read vision prescriptions: \(error.localizedDescription) ***") - } - ``` - - - Important: Using the ``requestAuthorization(toShare:read:)`` method to request read access to any data types that require per-object authorization fails with an ``HKError.Code.errorInvalidArgument`` error. - - When your app calls this method, HealthKit displays an authorization sheet that asks for permission to read the samples that match the predicate and object type. The person using your app can then select individual samples to share with your app. The system always asks for permission, regardless of whether they previously granted it. - - After the person responds, the system calls the callback handler on an arbitrary background queue. - - - Parameter objectType: The data type you want to read. - - Parameter predicate: A predicate that further restricts the data type. - - Parameter completion: A completion handler that the system calls after the user responds to the request. The completion handler has the following parameters: - - Parameter success: A Boolean value that indicates whether the request succeeded. This value doesn’t indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, it’s true. - - Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, the system passes `nil`. - */ - func requestPerObjectReadAuthorization( - for objectType: HKObjectType, - predicate: NSPredicate?, - completion: @escaping (Bool, Error?) -> Void - ) - - /** - Requests permission to save and read the data types specified by an extension. - - - Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration: - ``` - func handleAuthorizationForExtension() async throws - ``` - For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously). - - The host app must implement the application delegate’s ``applicationShouldRequestHealthAuthorization(_:)`` method. - This delegate method is called after an app extension calls ``requestAuthorization(toShare:read:completion:)``. - The host app is then responsible for calling `handleAuthorizationForExtension(completion:)`. - This method prompts the user to authorize both the app and its extensions for the types that the extension requested. - - The system performs this request asynchronously. - After the user has finished responding, this method calls its completion block on a background queue. - If the user has already chosen to grant or prohibit access to all of the types specified, the completion is called without prompting the user. - - - Parameters: - - completion: A block that is called after the user finishes responding to the request. This block is passed the following parameters: - - success: A Boolean value that indicates whether the user responded to the prompt (if any). This value does not indicate whether permission was actually granted. This parameter is false if the user canceled the prompt without selecting permissions; otherwise, true. - - error: An error object. If an error occurred, this object contains information about the error; otherwise, it is set to `nil`. - */ - func handleAuthorizationForExtension(completion: @escaping (Bool, Error?) -> Void) - - /** - The view controller that presents HealthKit authorization sheets. - - By default, the system infers the correct view controller to show HealthKit’s authorization sheet. - In some cases, you can improve the user experience by explicitly defining how the system presents the authentication sheets. - In particular, consider setting this property when using HealthKit in an iPadOS app. - */ - //@available(iOS 17.0, macCatalyst 17.0, visionOS 1.0, *) - //var authorizationViewControllerPresenter: UIViewController? { get set } - - // MARK: - Querying HealthKit data - - /** - Augments a `HKSampleQuery`. - - This function performs the same action as when creating a `HKSampleQuery` and executing it on a `HKHealthStore`. - - It's necessary to extract this function since not all properties of a `HKSampleType` can be accessed. - */ - func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery -} diff --git a/HealthImport/ActivityDetailView.swift b/HealthImport/ActivityDetailView.swift index 3a46854..3a945ab 100644 --- a/HealthImport/ActivityDetailView.swift +++ b/HealthImport/ActivityDetailView.swift @@ -1,48 +1,60 @@ import SwiftUI import HealthKit +import HKDatabase +import CoreLocation struct ActivityDetailView: View { - @EnvironmentObject - var database: HealthDatabase - let activity: HKWorkoutActivity - @State var locations: [LocationSample] = [] + @State var locations: [CLLocation] = [] @State var sampleCount: Int = 0 + private var metadata: [(key: String, value: Any)] { + activity.metadata?.sorted { $0.key } ?? [] + } + var body: some View { List { - DetailRow("UUID", value: activity.uuid) - DetailRow("Activity", value: activity.workoutConfiguration.activityType) - DetailRow("Location", value: activity.workoutConfiguration.locationType) - DetailRow("Swimming Location", value: activity.workoutConfiguration.swimmingLocationType) - DetailRow("Lap Length", value: activity.workoutConfiguration.lapLength) - DetailRow("Start", date: activity.startDate) - DetailRow("End", date: activity.endDate) - DetailRow("Duration", duration: activity.duration) - DetailRow("Metadata", value: activity.metadata) - if !locations.isEmpty { - NavigationLink(value: locations) { - DetailRow("Locations", value: "\(locations.count)") - } - } else { - DetailRow("Locations", value: "0") + Section("Properties") { + DetailRow("UUID", value: activity.uuid) + DetailRow("Activity", value: activity.workoutConfiguration.activityType) + DetailRow("Location", value: activity.workoutConfiguration.locationType) + DetailRow("Swimming Location", value: activity.workoutConfiguration.swimmingLocationType) + DetailRow("Lap Length", value: activity.workoutConfiguration.lapLength) + DetailRow("Start", date: activity.startDate) + DetailRow("End", date: activity.endDate) + DetailRow("Duration", duration: activity.duration) } - if sampleCount != 0 { - NavigationLink { - ActivitySamplesView(activity: activity) - .environmentObject(database) - } label: { - DetailRow("Samples", value: "\(sampleCount)") + Section("Data") { + if !locations.isEmpty { + NavigationLink(value: locations) { + DetailRow("Locations", value: "\(locations.count)") + } + } else { + DetailRow("Locations", value: "0") + } + if sampleCount != 0 { + NavigationLink { + ActivitySamplesView(activity: activity) + } label: { + DetailRow("Samples", value: "\(sampleCount)") + } + } else { + DetailRow("Samples", value: "0") + } + } + if !(activity.metadata?.isEmpty ?? true) { + Section("Metadata") { + ForEach(metadata, id: \.key) { (key, value) in + DetailRow(key, value: "\(value)") + } } - } else { - DetailRow("Samples", value: "0") } } .navigationTitle("Activity") - .navigationDestination(for: [LocationSample].self) { locations in + .navigationDestination(for: [CLLocation].self) { locations in LocationSampleListView(samples: locations) } .onAppear(perform: load) @@ -51,12 +63,12 @@ struct ActivityDetailView: View { private func load() { Task { do { - let samples = try database.locationSamples(for: activity) + let samples = try HealthDatabase.shared.locationSamples(for: activity) .sorted { $0.timestamp } - let sampleCount = try database.sampleCount(for: activity) + //let sampleCount = try HealthDatabase.shared.sampleCount(for: activity) DispatchQueue.main.async { self.locations = samples - self.sampleCount = sampleCount + //self.sampleCount = sampleCount } } catch { print("Failed to load location samples for activity: \(error)") @@ -66,8 +78,8 @@ struct ActivityDetailView: View { } #Preview { - NavigationStack { + HealthDatabase.shared = .mock() + return NavigationStack { ActivityDetailView(activity: .mock1) - .environmentObject(HealthDatabase.mock()) } } diff --git a/HealthImport/ActivitySamplesView.swift b/HealthImport/ActivitySamplesView.swift index 3d857a9..5e82768 100644 --- a/HealthImport/ActivitySamplesView.swift +++ b/HealthImport/ActivitySamplesView.swift @@ -1,15 +1,13 @@ import SwiftUI import OrderedCollections import HealthKit +import HKDatabase struct ActivitySamplesView: View { - @EnvironmentObject - var database: HealthDatabase - let activity: HKWorkoutActivity - @State var samples: [(type: Sample.DataType, samples: [Sample])] = [] + @State var samples: [(type: HKSampleType, samples: [HKSample])] = [] @State var timeZones: [TimeZone] = [] @@ -45,13 +43,15 @@ struct ActivitySamplesView: View { } private func loadAsync() { + #warning("Load samples for activity") + /* do { - let samples = try database.samples(for: activity) + let samples = try HealthDatabase.shared.samples(for: activity) let ordered = samples .sorted(using: { $0.key.rawValue }) .map { (type: $0, samples: $1) } let timeZones: Set = samples.reduce(into: Set()) { timeZones, sample in - timeZones.formUnion(sample.value.compactMap { $0.timeZone }) + timeZones.formUnion(sample.compactMap { $0.timeZone }) } DispatchQueue.main.async { @@ -62,6 +62,7 @@ struct ActivitySamplesView: View { } catch { print("Failed to load samples: \(error)") } + */ } } diff --git a/HealthImport/ContentView.swift b/HealthImport/ContentView.swift index 39cdf7d..a3669ee 100644 --- a/HealthImport/ContentView.swift +++ b/HealthImport/ContentView.swift @@ -1,17 +1,18 @@ import SwiftUI import HealthKit +import HKDatabase struct ContentView: View { - @StateObject var database: HealthDatabase - @State var navigationPath: NavigationPath = .init() - + + @State var workouts: [Workout] = [] + var body: some View { NavigationStack(path: $navigationPath) { VStack { List { - ForEach(database.workouts) { workout in + ForEach(workouts) { workout in NavigationLink(value: workout) { VStack(alignment: .leading) { Text(workout.typeString) @@ -26,12 +27,50 @@ struct ContentView: View { .navigationTitle("Workouts") .navigationDestination(for: Workout.self) { WorkoutDetailView(workout: $0) - .environmentObject(database) + } + .toolbar { + ToolbarItem { + Button(action: addWorkouts) { + Text("Add") + } + } + } + } + } + + private func addWorkouts() { + Task { + do { + try await insertExamplesOfAllTypes() + } catch { + print("Failed: \(error)") } } } } #Preview { - ContentView(database: .mock()) + HealthDatabase.shared = .mock() + return ContentView() } + +/* + data_series.hfd_key -> location_series_data.series_identifier + + samples.data_id = data_series.data_id + samples.data_id = quantity_samples.data_id + samples.data_id = objects.data_id + samples.data_id = quantity_sample_series.data_id + + objects.provenance = data_provenances.ROWID + + data_provenances.tz_name + data_provenances.origin_device + + Samples -> Quantity Sample Series -> Quantity Series Data + Samples -> Data Series + + + quantity_sample_series.hfd_key = quantity_series_data.series_identifier + + */ diff --git a/HealthImport/HealthDatabase.swift b/HealthImport/HealthDatabase.swift deleted file mode 100644 index 9f6d84f..0000000 --- a/HealthImport/HealthDatabase.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Foundation -import SQLite -import CoreLocation -import HealthKit - -typealias Database = Connection - -final class HealthDatabase: ObservableObject { - - private let fileUrl: URL - - private let database: Connection - - private let samples: SamplesTable - - private let workoutsTable: WorkoutsTable - - private let locationSamples: LocationSeriesDataTable - - private let dataSeries: DataSeriesTable - - @Published - var workouts: [Workout] = [] - - convenience init(fileUrl: URL) throws { - let database = try Connection(fileUrl.path) - self.init(fileUrl: fileUrl, database: database) - } - - init(fileUrl: URL, database: Connection) { - self.fileUrl = fileUrl - self.database = database - self.samples = .init(database: database) - self.workoutsTable = .init(database: database) - self.locationSamples = .init(database: database) - self.dataSeries = .init(database: database) - - DispatchQueue.global().async { - self.readAllWorkouts() - } - } - - func readAllWorkouts() { - let workouts: [Workout] - do { - workouts = try workoutsTable.workouts() - } catch { - print("Failed to read workouts: \(error)") - return - } - DispatchQueue.main.async { - self.workouts = workouts - } - } - - func locationSamples(for activity: HKWorkoutActivity) throws -> [LocationSample] { - try locationSamples.locationSamples(from: activity.startDate, to: activity.currentEndDate) - } - - func locationSampleCount(for activity: HKWorkoutActivity) throws -> Int { - try locationSamples.locationSampleCount(from: activity.startDate, to: activity.currentEndDate) - } - - func samples(for activity: HKWorkoutActivity) throws -> [Sample.DataType : [Sample]] { - try samples.samples(from: activity.startDate, to: activity.currentEndDate).reduce(into: [:]) { - $0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1] - } - } - - func sampleCount(for activity: HKWorkoutActivity) throws -> Int { - try samples.sampleCount(from: activity.startDate, to: activity.currentEndDate) - } - - var activities: [HKWorkoutActivity] { - workouts.map { $0.activities }.joined().sorted() - } - - private func testActivityOverlap() { - let activities = self.activities - var current = activities.first! - for next in activities.dropFirst() { - let overlap = next.startDate.timeIntervalSince(current.currentEndDate) - if overlap < 0 { - print("Overlap \(-overlap.roundedInt) s:") - print(" Activity \(current.workoutConfiguration.activityType.description): \(current.startDate.timeAndDateText) -> \(current.currentEndDate.timeAndDateText)") - print(" Activity \(next.workoutConfiguration.activityType.description): \(next.startDate.timeAndDateText) -> \(next.currentEndDate.timeAndDateText)") - } - current = next - } - } - - convenience init(database: Database) { - self.init(fileUrl: .init(filePath: "/"), database: database) - } - - func insert(workout: Workout) throws { - try workoutsTable.insert(workout) - } - - func insert(workout: Workout, into store: HKHealthStore) async throws -> HKWorkout? { - guard let configuration = workout.activities.first?.workoutConfiguration else { - return nil - } - - let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil) - return try await builder.finishWorkout() - } - - func createTables() throws { - try samples.createAll() - try workoutsTable.createAll() - try locationSamples.create(references: dataSeries) - } -} - -private extension HKWorkoutActivity { - - var currentEndDate: Date { - endDate ?? Date() - } -} diff --git a/HealthImport/HealthImport.entitlements b/HealthImport/HealthImport.entitlements new file mode 100644 index 0000000..2ab14a2 --- /dev/null +++ b/HealthImport/HealthImport.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + + diff --git a/HealthImport/HealthImportApp.swift b/HealthImport/HealthImportApp.swift index fa9f76e..d206623 100644 --- a/HealthImport/HealthImportApp.swift +++ b/HealthImport/HealthImportApp.swift @@ -1,15 +1,23 @@ import SwiftUI +import HKDatabase + +#warning("Load workouts from database and samples from HealthKit") @main struct HealthImportApp: App { var body: some Scene { WindowGroup { - ContentView(database: .init()) + ContentView() } } } +extension HealthDatabase { + + static var shared: HealthDatabase = .init() +} + private extension HealthDatabase { static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite") diff --git a/HealthImport/LocationSampleDetailView.swift b/HealthImport/LocationSampleDetailView.swift index 8255089..6432f99 100644 --- a/HealthImport/LocationSampleDetailView.swift +++ b/HealthImport/LocationSampleDetailView.swift @@ -1,8 +1,9 @@ import SwiftUI +import CoreLocation struct LocationSampleDetailView: View { - let location: LocationSample + let location: CLLocation var body: some View { List { diff --git a/HealthImport/LocationSampleListView.swift b/HealthImport/LocationSampleListView.swift index 3feffa9..5bd3e02 100644 --- a/HealthImport/LocationSampleListView.swift +++ b/HealthImport/LocationSampleListView.swift @@ -1,8 +1,9 @@ import SwiftUI +import CoreLocation struct LocationSampleListView: View { - let samples: [LocationSample] + let samples: [CLLocation] var body: some View { List { @@ -13,7 +14,7 @@ struct LocationSampleListView: View { } } .navigationTitle("Locations") - .navigationDestination(for: LocationSample.self) { + .navigationDestination(for: CLLocation.self) { LocationSampleDetailView(location: $0) } } diff --git a/HealthImport/Model/EventMetadata.pb.swift b/HealthImport/Model/EventMetadata.pb.swift deleted file mode 100644 index 0433058..0000000 --- a/HealthImport/Model/EventMetadata.pb.swift +++ /dev/null @@ -1,208 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: EventMetadata.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// Wrapper for workout event metadata -struct WorkoutEventMetadata { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// All metadata elements - var elements: [WorkoutEventMetadata.Element] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - struct Element { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var key: String = String() - - var unsignedValue: UInt64 { - get {return _unsignedValue ?? 0} - set {_unsignedValue = newValue} - } - /// Returns true if `unsignedValue` has been explicitly set. - var hasUnsignedValue: Bool {return self._unsignedValue != nil} - /// Clears the value of `unsignedValue`. Subsequent reads from it will return its default value. - mutating func clearUnsignedValue() {self._unsignedValue = nil} - - var quantity: WorkoutEventMetadata.Element.Quantity { - get {return _quantity ?? WorkoutEventMetadata.Element.Quantity()} - set {_quantity = newValue} - } - /// Returns true if `quantity` has been explicitly set. - var hasQuantity: Bool {return self._quantity != nil} - /// Clears the value of `quantity`. Subsequent reads from it will return its default value. - mutating func clearQuantity() {self._quantity = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - struct Quantity { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var value: Double = 0 - - var unit: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - init() {} - - fileprivate var _unsignedValue: UInt64? = nil - fileprivate var _quantity: WorkoutEventMetadata.Element.Quantity? = nil - } - - init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension WorkoutEventMetadata: @unchecked Sendable {} -extension WorkoutEventMetadata.Element: @unchecked Sendable {} -extension WorkoutEventMetadata.Element.Quantity: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -extension WorkoutEventMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "WorkoutEventMetadata" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "elements"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.elements) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.elements.isEmpty { - try visitor.visitRepeatedMessageField(value: self.elements, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: WorkoutEventMetadata, rhs: WorkoutEventMetadata) -> Bool { - if lhs.elements != rhs.elements {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension WorkoutEventMetadata.Element: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = WorkoutEventMetadata.protoMessageName + ".Element" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "key"), - 4: .same(proto: "unsignedValue"), - 6: .same(proto: "quantity"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.key) }() - case 4: try { try decoder.decodeSingularUInt64Field(value: &self._unsignedValue) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._quantity) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.key.isEmpty { - try visitor.visitSingularStringField(value: self.key, fieldNumber: 1) - } - try { if let v = self._unsignedValue { - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) - } }() - try { if let v = self._quantity { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: WorkoutEventMetadata.Element, rhs: WorkoutEventMetadata.Element) -> Bool { - if lhs.key != rhs.key {return false} - if lhs._unsignedValue != rhs._unsignedValue {return false} - if lhs._quantity != rhs._quantity {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension WorkoutEventMetadata.Element.Quantity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = WorkoutEventMetadata.Element.protoMessageName + ".Quantity" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "value"), - 2: .same(proto: "unit"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.value) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.unit) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.value != 0 { - try visitor.visitSingularDoubleField(value: self.value, fieldNumber: 1) - } - if !self.unit.isEmpty { - try visitor.visitSingularStringField(value: self.unit, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: WorkoutEventMetadata.Element.Quantity, rhs: WorkoutEventMetadata.Element.Quantity) -> Bool { - if lhs.value != rhs.value {return false} - if lhs.unit != rhs.unit {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/HealthImport/Model/Goal.swift b/HealthImport/Model/Goal.swift deleted file mode 100644 index 557ba9c..0000000 --- a/HealthImport/Model/Goal.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation - -enum Goal { - - case time(TimeInterval) - - init?(goalType: Int?, goal: Double?) { - switch goalType { - case .none: - return nil - case 0: - return nil - case 2: - guard let goal else { - print("Time goal, but no goal value set") - return nil - } - self = .time(goal) - default: - return nil - } - } - - var gaol: Double { - switch self { - case .time(let timeInterval): - return timeInterval - } - } - - var goalType: Int { - switch self { - case .time: return 2 - } - } -} - -extension Goal: CustomStringConvertible { - - var description: String { - switch self { - case .time(let seconds): - return seconds.durationString - } - } -} diff --git a/HealthImport/Model/HKWorkoutActivity+Comparable.swift b/HealthImport/Model/HKWorkoutActivity+Comparable.swift deleted file mode 100644 index 912b5f7..0000000 --- a/HealthImport/Model/HKWorkoutActivity+Comparable.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import HealthKit - -extension HKWorkoutActivity: Comparable { - - public static func < (lhs: HKWorkoutActivity, rhs: HKWorkoutActivity) -> Bool { - lhs.startDate < rhs.startDate - } -} - -extension HKWorkoutActivity { - - var externalUUID: UUID? { - guard let string = metadata?[HKMetadataKeyExternalUUID] as? String else { - return nil - } - return UUID(uuidString: string) - } -} diff --git a/HealthImport/Model/HKWorkoutEvent+Identifiable.swift b/HealthImport/Model/HKWorkoutEvent+Identifiable.swift deleted file mode 100644 index 64476bf..0000000 --- a/HealthImport/Model/HKWorkoutEvent+Identifiable.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import HealthKit -import SwiftProtobuf - -extension HKWorkoutEvent: Identifiable { - - public var id: Double { - dateInterval.start.timeIntervalSinceReferenceDate * Double(type.rawValue) * dateInterval.duration - } -} diff --git a/HealthImport/Model/MetadataKey.swift b/HealthImport/Model/MetadataKey.swift deleted file mode 100644 index 1f03e8f..0000000 --- a/HealthImport/Model/MetadataKey.swift +++ /dev/null @@ -1,329 +0,0 @@ -import Foundation - -enum Metadata { - -} - -extension Metadata { - - enum Key { - case wasUserEntered - case heartRateSensorLocation - case sessionId - case timeZone - case subIndex - case indoorWorkout - case privateMediaSourceBundleIdentifier - case sexualActivityProtectionUsed - case privateSleepAlarmUserWakeTime - case privateSleepAlarmUserSetBedtime - case devicePlacementSide - case privateHeadphoneAudioDataIsTransient - case privateCoreMotionSourceIdentifier - case privateHeartRateContext - case algorithmVersion - case privateAppleHeartbeatSeriesAlgorithmVersion - case barometricPressure - case privateBloodOxygenContext - case privateHeartbeatSequenceContext - case appleECGAlgorithmVersion - case privateRegulatedUpdateVersion - case privateActivitySummaryIndex - case privateDeepBreathingEndReason - case privateDeepBreathingFinalHeartRate - case privateMindfulnessSessionType - case privateWorkoutAverageHeartRate - case privateWorkoutMaxHeartRate - case privateWeatherCondition - case privateWorkoutWeatherLocationCoordinatesLongitude - case privateWorkoutMinHeartRate - case privateWorkoutWasInDaytime - case privateWorkoutActivityMoveMode - case averageMETs - case privateWorkoutWeatherLocationCoordinatesLatitude - case privateLostGPSAtSomePoint - case weatherTemperature - case privateWorkoutMinGroundElevation - case weatherHumidity - case privateWorkoutMaxGroundElevation - case elevationAscended - case privateWorkoutAverageCadence - case vO2MaxTestType - case privateUserOnBetaBlocker - case swimmingLocationType - case lapLength - case swimmingStrokeStyle - case audioExposureLevel - case privateAudioExposureLimit - case dateOfEarliestDataUsedForEstimate - case appleDeviceCalibrated - case heartRateEventThreshold - case privateHeartRateEventThreshold - case privateWorkoutTargetZoneMax - case privateWorkoutTargetZoneType - case privateWorkoutTargetZoneMin - case privateFallActionRequested - case glassesPrescriptionDescription - case privateWorkoutElapsedTimeInHeartRateZones - case privateWorkoutWeatherSourceName - case privateWorkoutHeartRateZones - case privateWorkoutConfiguration - case privateWorkoutHeartRateZonesConfigurationType - case privateWorkoutAveragePower - case privateMetricPlatterStatistics - case userMotionContext - case heartRateRecoveryActivityDuration - case heartRateRecoveryTestType - case heartRateRecoveryMaxObservedRecoveryHeartRate - case heartRateRecoveryActivityType - case sessionEstimate - case privateWorkoutExtendedMode - case unknown(String) - } -} - -extension Metadata.Key: RawRepresentable { - - init(rawValue: String) { - switch rawValue { - case "HKWasUserEntered": self = .wasUserEntered - case "HKHeartRateSensorLocation": self = .heartRateSensorLocation - case "sessionId": self = .sessionId - case "HKTimeZone": self = .timeZone - case "subIndex": self = .subIndex - case "HKIndoorWorkout": self = .indoorWorkout - case "_HKPrivateMediaSourceBundleIdentifier": self = .privateMediaSourceBundleIdentifier - case "HKSexualActivityProtectionUsed": self = .sexualActivityProtectionUsed - case "_HKPrivateSleepAlarmUserWakeTime": self = .privateSleepAlarmUserWakeTime - case "_HKPrivateSleepAlarmUserSetBedtime": self = .privateSleepAlarmUserSetBedtime - case "HKMetadataKeyDevicePlacementSide": self = .devicePlacementSide - case "_HKPrivateMetadataKeyHeadphoneAudioDataIsTransient": self = .privateHeadphoneAudioDataIsTransient - case "_HKPrivateCoreMotionSourceIdentifier": self = .privateCoreMotionSourceIdentifier - case "_HKPrivateHeartRateContext": self = .privateHeartRateContext - case "HKAlgorithmVersion": self = .algorithmVersion - case "_HKPrivateMetadataKeyAppleHeartbeatSeriesAlgorithmVersion": self = .privateAppleHeartbeatSeriesAlgorithmVersion - case "HKMetadataKeyBarometricPressure": self = .barometricPressure - case "_HKPrivateBloodOxygenContext": self = .privateBloodOxygenContext - case "_HKPrivateHeartbeatSequenceContext": self = .privateHeartbeatSequenceContext - case "HKMetadataKeyAppleECGAlgorithmVersion": self = .appleECGAlgorithmVersion - case "_HKPrivateMetadataKeyRegulatedUpdateVersion": self = .privateRegulatedUpdateVersion - case "_HKPrivateActivitySummaryIndex": self = .privateActivitySummaryIndex - case "_HKPrivateDeepBreathingEndReason": self = .privateDeepBreathingEndReason - case "_HKPrivateDeepBreathingFinalHeartRate": self = .privateDeepBreathingFinalHeartRate - case "_HKPrivateMetadataMindfulnessSessionType": self = .privateMindfulnessSessionType - case "_HKPrivateWorkoutAverageHeartRate": self = .privateWorkoutAverageHeartRate - case "_HKPrivateWorkoutMaxHeartRate": self = .privateWorkoutMaxHeartRate - case "_HKPrivateWeatherCondition": self = .privateWeatherCondition - case "_HKPrivateWorkoutWeatherLocationCoordinatesLongitude": self = .privateWorkoutWeatherLocationCoordinatesLongitude - case "_HKPrivateWorkoutMinHeartRate": self = .privateWorkoutMinHeartRate - case "_HKPrivateWorkoutWasInDaytime": self = .privateWorkoutWasInDaytime - case "_HKPrivateWorkoutActivityMoveMode": self = .privateWorkoutActivityMoveMode - case "HKAverageMETs": self = .averageMETs - case "_HKPrivateWorkoutWeatherLocationCoordinatesLatitude": self = .privateWorkoutWeatherLocationCoordinatesLatitude - case "_HKPrivateMetadataKeyLostGPSAtSomePoint": self = .privateLostGPSAtSomePoint - case "HKWeatherTemperature": self = .weatherTemperature - case "_HKPrivateWorkoutMinGroundElevation": self = .privateWorkoutMinGroundElevation - case "HKWeatherHumidity": self = .weatherHumidity - case "_HKPrivateWorkoutMaxGroundElevation": self = .privateWorkoutMaxGroundElevation - case "HKElevationAscended": self = .elevationAscended - case "_HKPrivateWorkoutAverageCadence": self = .privateWorkoutAverageCadence - case "HKVO2MaxTestType": self = .vO2MaxTestType - case "_HKPrivateMetadataKeyUserOnBetaBlocker": self = .privateUserOnBetaBlocker - case "HKSwimmingLocationType": self = .swimmingLocationType - case "HKLapLength": self = .lapLength - case "HKSwimmingStrokeStyle": self = .swimmingStrokeStyle - case "HKMetadataKeyAudioExposureLevel": self = .audioExposureLevel - case "_HKPrivateMetadataKeyAudioExposureLimit": self = .privateAudioExposureLimit - case "HKDateOfEarliestDataUsedForEstimate": self = .dateOfEarliestDataUsedForEstimate - case "HKMetadataKeyAppleDeviceCalibrated": self = .appleDeviceCalibrated - case "HKHeartRateEventThreshold": self = .heartRateEventThreshold - case "_HKPrivateMetadataKeyHeartRateEventThreshold": self = .privateHeartRateEventThreshold - case "_HKPrivateWorkoutTargetZoneMax": self = .privateWorkoutTargetZoneMax - case "_HKPrivateWorkoutTargetZoneType": self = .privateWorkoutTargetZoneType - case "_HKPrivateWorkoutTargetZoneMin": self = .privateWorkoutTargetZoneMin - case "_HKPrivateFallActionRequested": self = .privateFallActionRequested - case "HKMetadataKeyGlassesPrescriptionDescription": self = .glassesPrescriptionDescription - case "_HKPrivateWorkoutElapsedTimeInHeartRateZones": self = .privateWorkoutElapsedTimeInHeartRateZones - case "_HKPrivateWorkoutWeatherSourceName": self = .privateWorkoutWeatherSourceName - case "_HKPrivateWorkoutHeartRateZones": self = .privateWorkoutHeartRateZones - case "_HKPrivateWorkoutConfiguration": self = .privateWorkoutConfiguration - case "_HKPrivateWorkoutHeartRateZonesConfigurationType": self = .privateWorkoutHeartRateZonesConfigurationType - case "_HKPrivateWorkoutAveragePower": self = .privateWorkoutAveragePower - case "_HKPrivateMetadataKeyMetricPlatterStatistics": self = .privateMetricPlatterStatistics - case "HKMetadataKeyUserMotionContext": self = .userMotionContext - case "HKMetadataKeyHeartRateRecoveryActivityDuration": self = .heartRateRecoveryActivityDuration - case "HKMetadataKeyHeartRateRecoveryTestType": self = .heartRateRecoveryTestType - case "HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate": self = .heartRateRecoveryMaxObservedRecoveryHeartRate - case "HKMetadataKeyHeartRateRecoveryActivityType": self = .heartRateRecoveryActivityType - case "HKMetadataKeySessionEstimate": self = .sessionEstimate - case "_HKPrivateWorkoutExtendedMode": self = .privateWorkoutExtendedMode - default: - self = .unknown(rawValue) - } - } - - var rawValue: String { - switch self { - case .wasUserEntered: return "HKWasUserEntered" - case .heartRateSensorLocation: return "HKHeartRateSensorLocation" - case .sessionId: return "sessionId" - case .timeZone: return "HKTimeZone" - case .subIndex: return "subIndex" - case .indoorWorkout: return "HKIndoorWorkout" - case .privateMediaSourceBundleIdentifier: return "_HKPrivateMediaSourceBundleIdentifier" - case .sexualActivityProtectionUsed: return "HKSexualActivityProtectionUsed" - case .privateSleepAlarmUserWakeTime: return "_HKPrivateSleepAlarmUserWakeTime" - case .privateSleepAlarmUserSetBedtime: return "_HKPrivateSleepAlarmUserSetBedtime" - case .devicePlacementSide: return "HKMetadataKeyDevicePlacementSide" - case .privateHeadphoneAudioDataIsTransient: return "_HKPrivateMetadataKeyHeadphoneAudioDataIsTransient" - case .privateCoreMotionSourceIdentifier: return "_HKPrivateCoreMotionSourceIdentifier" - case .privateHeartRateContext: return "_HKPrivateHeartRateContext" - case .algorithmVersion: return "HKAlgorithmVersion" - case .privateAppleHeartbeatSeriesAlgorithmVersion: return "_HKPrivateMetadataKeyAppleHeartbeatSeriesAlgorithmVersion" - case .barometricPressure: return "HKMetadataKeyBarometricPressure" - case .privateBloodOxygenContext: return "_HKPrivateBloodOxygenContext" - case .privateHeartbeatSequenceContext: return "_HKPrivateHeartbeatSequenceContext" - case .appleECGAlgorithmVersion: return "HKMetadataKeyAppleECGAlgorithmVersion" - case .privateRegulatedUpdateVersion: return "_HKPrivateMetadataKeyRegulatedUpdateVersion" - case .privateActivitySummaryIndex: return "_HKPrivateActivitySummaryIndex" - case .privateDeepBreathingEndReason: return "_HKPrivateDeepBreathingEndReason" - case .privateDeepBreathingFinalHeartRate: return "_HKPrivateDeepBreathingFinalHeartRate" - case .privateMindfulnessSessionType: return "_HKPrivateMetadataMindfulnessSessionType" - case .privateWorkoutAverageHeartRate: return "_HKPrivateWorkoutAverageHeartRate" - case .privateWorkoutMaxHeartRate: return "_HKPrivateWorkoutMaxHeartRate" - case .privateWeatherCondition: return "_HKPrivateWeatherCondition" - case .privateWorkoutWeatherLocationCoordinatesLongitude: return "_HKPrivateWorkoutWeatherLocationCoordinatesLongitude" - case .privateWorkoutMinHeartRate: return "_HKPrivateWorkoutMinHeartRate" - case .privateWorkoutWasInDaytime: return "_HKPrivateWorkoutWasInDaytime" - case .privateWorkoutActivityMoveMode: return "_HKPrivateWorkoutActivityMoveMode" - case .averageMETs: return "HKAverageMETs" - case .privateWorkoutWeatherLocationCoordinatesLatitude: return "_HKPrivateWorkoutWeatherLocationCoordinatesLatitude" - case .privateLostGPSAtSomePoint: return "_HKPrivateMetadataKeyLostGPSAtSomePoint" - case .weatherTemperature: return "HKWeatherTemperature" - case .privateWorkoutMinGroundElevation: return "_HKPrivateWorkoutMinGroundElevation" - case .weatherHumidity: return "HKWeatherHumidity" - case .privateWorkoutMaxGroundElevation: return "_HKPrivateWorkoutMaxGroundElevation" - case .elevationAscended: return "HKElevationAscended" - case .privateWorkoutAverageCadence: return "_HKPrivateWorkoutAverageCadence" - case .vO2MaxTestType: return "HKVO2MaxTestType" - case .privateUserOnBetaBlocker: return "_HKPrivateMetadataKeyUserOnBetaBlocker" - case .swimmingLocationType: return "HKSwimmingLocationType" - case .lapLength: return "HKLapLength" - case .swimmingStrokeStyle: return "HKSwimmingStrokeStyle" - case .audioExposureLevel: return "HKMetadataKeyAudioExposureLevel" - case .privateAudioExposureLimit: return "_HKPrivateMetadataKeyAudioExposureLimit" - case .dateOfEarliestDataUsedForEstimate: return "HKDateOfEarliestDataUsedForEstimate" - case .appleDeviceCalibrated: return "HKMetadataKeyAppleDeviceCalibrated" - case .heartRateEventThreshold: return "HKHeartRateEventThreshold" - case .privateHeartRateEventThreshold: return "_HKPrivateMetadataKeyHeartRateEventThreshold" - case .privateWorkoutTargetZoneMax: return "_HKPrivateWorkoutTargetZoneMax" - case .privateWorkoutTargetZoneType: return "_HKPrivateWorkoutTargetZoneType" - case .privateWorkoutTargetZoneMin: return "_HKPrivateWorkoutTargetZoneMin" - case .privateFallActionRequested: return "_HKPrivateFallActionRequested" - case .glassesPrescriptionDescription: return "HKMetadataKeyGlassesPrescriptionDescription" - case .privateWorkoutElapsedTimeInHeartRateZones: return "_HKPrivateWorkoutElapsedTimeInHeartRateZones" - case .privateWorkoutWeatherSourceName: return "_HKPrivateWorkoutWeatherSourceName" - case .privateWorkoutHeartRateZones: return "_HKPrivateWorkoutHeartRateZones" - case .privateWorkoutConfiguration: return "_HKPrivateWorkoutConfiguration" - case .privateWorkoutHeartRateZonesConfigurationType: return "_HKPrivateWorkoutHeartRateZonesConfigurationType" - case .privateWorkoutAveragePower: return "_HKPrivateWorkoutAveragePower" - case .privateMetricPlatterStatistics: return "_HKPrivateMetadataKeyMetricPlatterStatistics" - case .userMotionContext: return "HKMetadataKeyUserMotionContext" - case .heartRateRecoveryActivityDuration: return "HKMetadataKeyHeartRateRecoveryActivityDuration" - case .heartRateRecoveryTestType: return "HKMetadataKeyHeartRateRecoveryTestType" - case .heartRateRecoveryMaxObservedRecoveryHeartRate: return "HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate" - case .heartRateRecoveryActivityType: return "HKMetadataKeyHeartRateRecoveryActivityType" - case .sessionEstimate: return "HKMetadataKeySessionEstimate" - case .privateWorkoutExtendedMode: return "_HKPrivateWorkoutExtendedMode" - case .unknown(let rawValue): - return rawValue - } - } -} - -extension Metadata.Key: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(rawValue) - } -} - -extension Metadata.Key: CustomStringConvertible { - - var description: String { - switch self { - case .wasUserEntered: return "Entered by user" - case .heartRateSensorLocation: return "Location of Heart Rate Sensor" - case .sessionId: return "Session ID" - case .timeZone: return "Timezone" - case .subIndex: return "Sub-Index" - case .indoorWorkout: return "Indoor Workout" - case .sexualActivityProtectionUsed: return "Protection Used for Sexual Activity" - case .privateMediaSourceBundleIdentifier: return "Media Source Bundle Identifier (private)" - case .privateSleepAlarmUserWakeTime: return "Sleep Alarm User Wake Time (private)" - case .privateSleepAlarmUserSetBedtime: return "Sleep Alarm User Set Bedtime (private)" - case .devicePlacementSide: return "Device Placement Side" - case .privateHeadphoneAudioDataIsTransient: return "Headphone Audio Data is Transient (private)" - case .privateCoreMotionSourceIdentifier: return "Core Motion Source Identifier (private)" - case .privateHeartRateContext: return "Heart Rate Context (private)" - case .algorithmVersion: return "Algorithm Version" - case .privateAppleHeartbeatSeriesAlgorithmVersion: return "Apple Heartbeat Series Algorithm Version (private)" - case .barometricPressure: return "Barometric Pressure" - case .privateBloodOxygenContext: return "Blood Oxygen Context (private)" - case .privateHeartbeatSequenceContext: return "Heartbeat Sequence Context (private)" - case .appleECGAlgorithmVersion: return "Apple ECG Algorithm Version" - case .privateRegulatedUpdateVersion: return "Regulated Update Version (private)" - case .privateActivitySummaryIndex: return "Activity Summary Index (private)" - case .privateDeepBreathingEndReason: return "Reason for End of Deep Breathing (private)" - case .privateDeepBreathingFinalHeartRate: return "Deep Breathing Final Heart Rate (private)" - case .privateMindfulnessSessionType: return "Mindfulness Session Type (private)" - case .privateWorkoutAverageHeartRate: return "Average Workout Heart Rate (private)" - case .privateWorkoutMaxHeartRate: return "Maximum Workout Heart Rate (private)" - case .privateWorkoutMinHeartRate: return "Minimum Workout Heart Rate (private)" - case .privateWorkoutWasInDaytime: return "Workout Was in Daytime (private)" - case .privateWorkoutActivityMoveMode: return "Workout Activity Move Mode (private)" - case .averageMETs: return "Average METs" - case .privateWorkoutWeatherLocationCoordinatesLatitude: return "Workout Weather Location Latitude (private)" - case .privateWorkoutWeatherLocationCoordinatesLongitude: return "Workout Weather Location Longitude (private)" - case .privateLostGPSAtSomePoint: return "Lost GPS At Some Point (private)" - case .privateWeatherCondition: return "Weather Condition (private)" - case .weatherTemperature: return "Weather Temperature" - case .weatherHumidity: return "Weather Humidity" - case .privateWorkoutMinGroundElevation: return "Minimum Ground Elevation for Workout (private)" - case .privateWorkoutMaxGroundElevation: return "Maximum Ground Elevation for Workout (private)" - case .elevationAscended: return "Ascended Elevation" - case .privateWorkoutAverageCadence: return "Average Workout Cadence (private)" - case .vO2MaxTestType: return "VO2Max Test Type" - case .privateUserOnBetaBlocker: return "User is on Beta Blocker (private)" - case .swimmingLocationType: return "Swimming Location Type" - case .lapLength: return "Lap Length" - case .swimmingStrokeStyle: return "Swimming Stroke Style" - case .audioExposureLevel: return "Audio Exposure Level" - case .privateAudioExposureLimit: return "Audio Exposure Limit (private)" - case .dateOfEarliestDataUsedForEstimate: return "Date of Earliest Data Used for Estimate" - case .appleDeviceCalibrated: return "Apple Device Calibrated" - case .heartRateEventThreshold: return "Heart Rate Event Threshold" - case .privateHeartRateEventThreshold: return "Heart Rate Event Threshold (private)" - case .privateWorkoutTargetZoneMax: return "Workout Target Zone Maximum (private)" - case .privateWorkoutTargetZoneType: return "Workout Target Zone Type (private)" - case .privateWorkoutTargetZoneMin: return "Workout Target Zone Minimum (private)" - case .privateFallActionRequested: return "Requested Fall Action (private)" - case .glassesPrescriptionDescription: return "Glasses Prescription Description" - case .privateWorkoutElapsedTimeInHeartRateZones: return "Workout Elapsed Time in Heart Rate Zones (private)" - case .privateWorkoutWeatherSourceName: return "Workout Weather Source Name (private)" - case .privateWorkoutHeartRateZones: return "Workout Heart Rate Zones (private)" - case .privateWorkoutConfiguration: return "Workout Configuration (private)" - case .privateWorkoutHeartRateZonesConfigurationType: return "Workout Heart Rate Zones Configuration Type (private)" - case .privateWorkoutAveragePower: return "Average Workout Power (private)" - case .privateMetricPlatterStatistics: return "Metric Platter Statistics (private)" - case .userMotionContext: return "User Motion Context" - case .heartRateRecoveryActivityDuration: return "Heart Rate Recovery Activity Duration" - case .heartRateRecoveryActivityType: return "Heart Rate Recovery Activity Type" - case .heartRateRecoveryTestType: return "Heart Rate Recovery Test Type" - case .heartRateRecoveryMaxObservedRecoveryHeartRate: return "Max. Observed Recovery Heart Rate" - case .sessionEstimate: return "Session Estimate" - case .privateWorkoutExtendedMode: return "Extended Workout Mode (private)" - case .unknown(let string): return string - } - } -} diff --git a/HealthImport/Model/MetadataValue.swift b/HealthImport/Model/MetadataValue.swift deleted file mode 100644 index 9ed5239..0000000 --- a/HealthImport/Model/MetadataValue.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -extension Metadata { - - enum Value { - - case string(value: String) - case number(value: Double) - case date(value: Date) - case numerical(value: Double, unit: String) - case data(value: Data) - - enum ValueType: Int { - - /// Uses only the `string_value` column - case string = 0 - - /// Uses only the `numerical_value` column - case number = 1 - - /// Uses only the `date_value` column - case date = 2 - - /// Uses the `string_value` column for the unit, and the `numerical_value` column for the number - case numerical = 3 - - /// Uses only the `data_value` column - case data = 4 - } - - var valueType: ValueType { - switch self { - case .string: return .string - case .number: return .number - case .date: return .date - case .numerical: return .numerical - case .data: return .data - } - } - } -} - -extension Metadata.Value: CustomStringConvertible { - - var description: String { - switch self { - case .string(let value): - return value - case .number(let value): - return "\(value)" - case .date(let value): - return value.timeAndDateText - case .numerical(let value, let unit): - return String(format: "%.3f ", value) + unit - case .data(let value): - return value.description - } - } -} diff --git a/HealthImport/Model/Sample.swift b/HealthImport/Model/Sample.swift deleted file mode 100644 index 2ad685c..0000000 --- a/HealthImport/Model/Sample.swift +++ /dev/null @@ -1,146 +0,0 @@ -import Foundation - -struct Sample { - - let startDate: Date - - let endDate: Date - - let dataType: DataType - - let quantity: Double? - - let originalQuantity: Double? - - let originalUnit: String? - - let timeZoneName: String? - - var timeZone: TimeZone? { - guard let timeZoneName else { - return nil - } - guard let zone = TimeZone(identifier: timeZoneName) else { - print("No time zone for '\(timeZoneName)'") - return nil - } - return zone - } - - var duration: TimeInterval { - endDate.timeIntervalSince(startDate) - } - - var originalQuantityText: String { - guard let originalQuantity, let originalUnit else { - return "" - } - return " (\(originalQuantity) \(originalUnit))" - - } - - var quantityText: String { - guard let quantity else { - return "-" - } - return "\(quantity)" - } - - var dateText: String { - startDate.timeAndDateText(in: timeZone ?? .current) - } -} - -extension Sample: CustomStringConvertible { - - var description: String { - "\(dateText) (\(Int(duration)) s) \(quantityText)\(originalQuantityText)" - } -} - -extension Sample { - - enum DataType: RawRepresentable { - - case weight // 3 - case heartRate // 5 - case stepCount // 7 - case distance // 8 - case restingEnergy // 9 - case activeEnergy // 10 - case flightsClimed // 12 - case weeklyCalorieGoal // 67 - case watchOn // 70 - case standMinutes // 75 - case activity // 76 - case workout // 79 - - case unknown(Int) - - init(rawValue: Int) { - switch rawValue { - case 3: self = .weight - case 5: self = .heartRate - case 7: self = .stepCount - case 8: self = .distance - case 9: self = .restingEnergy - case 10: self = .activeEnergy - case 12: self = .flightsClimed - case 67: self = .weeklyCalorieGoal - case 70: self = .watchOn - case 75: self = .standMinutes - case 76: self = .activity - case 79: self = .workout - default: - self = .unknown(rawValue) - } - } - - var rawValue: Int { - switch self { - case .stepCount: return 7 - case .weight: return 3 - case .heartRate: return 5 - case .distance: return 8 - case .restingEnergy: return 9 - case .activeEnergy: return 10 - case .flightsClimed: return 12 - case .weeklyCalorieGoal: return 67 - case .watchOn: return 70 - case .standMinutes: return 75 - case .activity: return 76 - case .workout: return 79 - case .unknown(let value): return value - } - } - } -} - -extension Sample.DataType: Equatable { - -} - -extension Sample.DataType: Hashable { - -} - -extension Sample.DataType: CustomStringConvertible { - - var description: String { - switch self { - case .stepCount: return "Step Count" - case .weight: return "Weight" - case .heartRate: return "Heart Rate" - case .distance: return "Distance" - case .restingEnergy: return "Resting Energy" - case .activeEnergy: return "Active Energy" - case .flightsClimed: return "Flights Climbed" - case .weeklyCalorieGoal: return "Weekly Calorie Goal" - case .watchOn: return "Watch On" - case .standMinutes: return "Stand Minutes" - case .activity: return "Activity" - case .workout: return "Workout" - case .unknown(let int): return "Unknown(\(int))" - } - } -} diff --git a/HealthImport/Model/Tables/DataProvenancesTable.swift b/HealthImport/Model/Tables/DataProvenancesTable.swift deleted file mode 100644 index 6473c6c..0000000 --- a/HealthImport/Model/Tables/DataProvenancesTable.swift +++ /dev/null @@ -1,266 +0,0 @@ -import Foundation -import SQLite -import HealthKit - -struct DataProvenancesTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - func create() throws { - try database.execute("CREATE TABLE data_provenances (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, sync_provenance INTEGER NOT NULL, origin_product_type TEXT NOT NULL, origin_build TEXT NOT NULL, local_product_type TEXT NOT NULL, local_build TEXT NOT NULL, source_id INTEGER NOT NULL, device_id INTEGER NOT NULL, contributor_id INTEGER NOT NULL, source_version TEXT NOT NULL, tz_name TEXT NOT NULL, origin_major_version INTEGER NOT NULL, origin_minor_version INTEGER NOT NULL, origin_patch_version INTEGER NOT NULL, sync_identity INTEGER NOT NULL, derived_flags INTEGER NOT NULL, UNIQUE(sync_provenance, origin_product_type, origin_build, local_product_type, local_build, source_id, device_id, contributor_id, source_version, tz_name, origin_major_version, origin_minor_version, origin_patch_version, sync_identity))") - } - - let table = Table("data_provenances") - - let rowId = Expression("ROWID") - - let syncProvenance = Expression("sync_provenance") - - /// Device that created the data (e.g. Watch) - let originProductType = Expression("origin_product_type") - - let originBuild = Expression("origin_build") - - /// Device saving the data (e.g. iPhone) - let localProductType = Expression("local_product_type") - - let localBuild = Expression("local_build") - - let sourceId = Expression("source_id") - - let deviceId = Expression("device_id") - - let contributorId = Expression("contributor_id") - - let sourceVersion = Expression("source_version") - - let tzName = Expression("tz_name") - - let originMajorVersion = Expression("origin_major_version") - - let originMinorVersion = Expression("origin_minor_version") - - let originPatchVersion = Expression("origin_patch_version") - - let syncIdentity = Expression("sync_identity") - - let derivedFlags = Expression("derived_flags") - - func device(for rowId: Int) throws -> HKDevice? { - try database.pluck(table.filter(self.rowId == rowId)).map { row in - let productType = row[originProductType] - return HKDevice( - name: nil, - manufacturer: "Apple Inc.", - model: productTypeToHumanName[productType], - hardwareVersion: productType, - firmwareVersion: nil, - softwareVersion: "\(row[originMajorVersion]).\(row[originMinorVersion]).\(row[originPatchVersion])", - localIdentifier: nil, - udiDeviceIdentifier: nil) - } - } - - func timeZoneName(for rowId: Int) throws -> String? { - try database.pluck(table.filter(self.rowId == rowId)).map { $0[tzName] } - } -} - -private let productTypeToHumanName: [String : String] = [ - "i386" : "iPhone Simulator", - "x86_64" : "iPhone Simulator", - "arm64" : "iPhone Simulator", - "iPhone1,1" : "iPhone", - "iPhone1,2" : "iPhone 3G", - "iPhone2,1" : "iPhone 3GS", - "iPhone3,1" : "iPhone 4", - "iPhone3,2" : "iPhone 4 GSM Rev A", - "iPhone3,3" : "iPhone 4 CDMA", - "iPhone4,1" : "iPhone 4S", - "iPhone5,1" : "iPhone 5 (GSM)", - "iPhone5,2" : "iPhone 5 (GSM+CDMA)", - "iPhone5,3" : "iPhone 5C (GSM)", - "iPhone5,4" : "iPhone 5C (Global)", - "iPhone6,1" : "iPhone 5S (GSM)", - "iPhone6,2" : "iPhone 5S (Global)", - "iPhone7,1" : "iPhone 6 Plus", - "iPhone7,2" : "iPhone 6", - "iPhone8,1" : "iPhone 6s", - "iPhone8,2" : "iPhone 6s Plus", - "iPhone8,4" : "iPhone SE (GSM)", - "iPhone9,1" : "iPhone 7", - "iPhone9,2" : "iPhone 7 Plus", - "iPhone9,3" : "iPhone 7", - "iPhone9,4" : "iPhone 7 Plus", - "iPhone10,1" : "iPhone 8", - "iPhone10,2" : "iPhone 8 Plus", - "iPhone10,3" : "iPhone X Global", - "iPhone10,4" : "iPhone 8", - "iPhone10,5" : "iPhone 8 Plus", - "iPhone10,6" : "iPhone X GSM", - "iPhone11,2" : "iPhone XS", - "iPhone11,4" : "iPhone XS Max", - "iPhone11,6" : "iPhone XS Max Global", - "iPhone11,8" : "iPhone XR", - "iPhone12,1" : "iPhone 11", - "iPhone12,3" : "iPhone 11 Pro", - "iPhone12,5" : "iPhone 11 Pro Max", - "iPhone12,8" : "iPhone SE 2nd Gen", - "iPhone13,1" : "iPhone 12 Mini", - "iPhone13,2" : "iPhone 12", - "iPhone13,3" : "iPhone 12 Pro", - "iPhone13,4" : "iPhone 12 Pro Max", - "iPhone14,2" : "iPhone 13 Pro", - "iPhone14,3" : "iPhone 13 Pro Max", - "iPhone14,4" : "iPhone 13 Mini", - "iPhone14,5" : "iPhone 13", - "iPhone14,6" : "iPhone SE 3rd Gen", - "iPhone14,7" : "iPhone 14", - "iPhone14,8" : "iPhone 14 Plus", - "iPhone15,2" : "iPhone 14 Pro", - "iPhone15,3" : "iPhone 14 Pro Max", - "iPhone15,4" : "iPhone 15", - "iPhone15,5" : "iPhone 15 Plus", - "iPhone16,1" : "iPhone 15 Pro", - "iPhone16,2" : "iPhone 15 Pro Max", - - "iPod1,1" : "1st Gen iPod", - "iPod2,1" : "2nd Gen iPod", - "iPod3,1" : "3rd Gen iPod", - "iPod4,1" : "4th Gen iPod", - "iPod5,1" : "5th Gen iPod", - "iPod7,1" : "6th Gen iPod", - "iPod9,1" : "7th Gen iPod", - - "iPad1,1" : "iPad", - "iPad1,2" : "iPad 3G", - "iPad2,1" : "2nd Gen iPad", - "iPad2,2" : "2nd Gen iPad GSM", - "iPad2,3" : "2nd Gen iPad CDMA", - "iPad2,4" : "2nd Gen iPad New Revision", - "iPad3,1" : "3rd Gen iPad", - "iPad3,2" : "3rd Gen iPad CDMA", - "iPad3,3" : "3rd Gen iPad GSM", - "iPad2,5" : "iPad mini", - "iPad2,6" : "iPad mini GSM+LTE", - "iPad2,7" : "iPad mini CDMA+LTE", - "iPad3,4" : "4th Gen iPad", - "iPad3,5" : "4th Gen iPad GSM+LTE", - "iPad3,6" : "4th Gen iPad CDMA+LTE", - "iPad4,1" : "iPad Air (WiFi)", - "iPad4,2" : "iPad Air (GSM+CDMA)", - "iPad4,3" : "1st Gen iPad Air (China)", - "iPad4,4" : "iPad mini Retina (WiFi)", - "iPad4,5" : "iPad mini Retina (GSM+CDMA)", - "iPad4,6" : "iPad mini Retina (China)", - "iPad4,7" : "iPad mini 3 (WiFi)", - "iPad4,8" : "iPad mini 3 (GSM+CDMA)", - "iPad4,9" : "iPad Mini 3 (China)", - "iPad5,1" : "iPad mini 4 (WiFi)", - "iPad5,2" : "4th Gen iPad mini (WiFi+Cellular)", - "iPad5,3" : "iPad Air 2 (WiFi)", - "iPad5,4" : "iPad Air 2 (Cellular)", - "iPad6,3" : "iPad Pro (9.7 inch, WiFi)", - "iPad6,4" : "iPad Pro (9.7 inch, WiFi+LTE)", - "iPad6,7" : "iPad Pro (12.9 inch, WiFi)", - "iPad6,8" : "iPad Pro (12.9 inch, WiFi+LTE)", - "iPad6,11" : "iPad (2017)", - "iPad6,12" : "iPad (2017)", - "iPad7,1" : "iPad Pro 2nd Gen (WiFi)", - "iPad7,2" : "iPad Pro 2nd Gen (WiFi+Cellular)", - "iPad7,3" : "iPad Pro 10.5-inch 2nd Gen", - "iPad7,4" : "iPad Pro 10.5-inch 2nd Gen", - "iPad7,5" : "iPad 6th Gen (WiFi)", - "iPad7,6" : "iPad 6th Gen (WiFi+Cellular)", - "iPad7,11" : "iPad 7th Gen 10.2-inch (WiFi)", - "iPad7,12" : "iPad 7th Gen 10.2-inch (WiFi+Cellular)", - "iPad8,1" : "iPad Pro 11 inch 3rd Gen (WiFi)", - "iPad8,2" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi)", - "iPad8,3" : "iPad Pro 11 inch 3rd Gen (WiFi+Cellular)", - "iPad8,4" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)", - "iPad8,5" : "iPad Pro 12.9 inch 3rd Gen (WiFi)", - "iPad8,6" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)", - "iPad8,7" : "iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)", - "iPad8,8" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)", - "iPad8,9" : "iPad Pro 11 inch 4th Gen (WiFi)", - "iPad8,10" : "iPad Pro 11 inch 4th Gen (WiFi+Cellular)", - "iPad8,11" : "iPad Pro 12.9 inch 4th Gen (WiFi)", - "iPad8,12" : "iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)", - "iPad11,1" : "iPad mini 5th Gen (WiFi)", - "iPad11,2" : "iPad mini 5th Gen", - "iPad11,3" : "iPad Air 3rd Gen (WiFi)", - "iPad11,4" : "iPad Air 3rd Gen", - "iPad11,6" : "iPad 8th Gen (WiFi)", - "iPad11,7" : "iPad 8th Gen (WiFi+Cellular)", - "iPad12,1" : "iPad 9th Gen (WiFi)", - "iPad12,2" : "iPad 9th Gen (WiFi+Cellular)", - "iPad14,1" : "iPad mini 6th Gen (WiFi)", - "iPad14,2" : "iPad mini 6th Gen (WiFi+Cellular)", - "iPad13,1" : "iPad Air 4th Gen (WiFi)", - "iPad13,2" : "iPad Air 4th Gen (WiFi+Cellular)", - "iPad13,4" : "iPad Pro 11 inch 5th Gen", - "iPad13,5" : "iPad Pro 11 inch 5th Gen", - "iPad13,6" : "iPad Pro 11 inch 5th Gen", - "iPad13,7" : "iPad Pro 11 inch 5th Gen", - "iPad13,8" : "iPad Pro 12.9 inch 5th Gen", - "iPad13,9" : "iPad Pro 12.9 inch 5th Gen", - "iPad13,10" : "iPad Pro 12.9 inch 5th Gen", - "iPad13,11" : "iPad Pro 12.9 inch 5th Gen", - "iPad13,16" : "iPad Air 5th Gen (WiFi)", - "iPad13,17" : "iPad Air 5th Gen (WiFi+Cellular)", - "iPad13,18" : "iPad 10th Gen", - "iPad13,19" : "iPad 10th Gen", - "iPad14,3" : "iPad Pro 11 inch 4th Gen", - "iPad14,4" : "iPad Pro 11 inch 4th Gen", - "iPad14,5" : "iPad Pro 12.9 inch 6th Gen", - "iPad14,6" : "iPad Pro 12.9 inch 6th Gen", - - "Watch1,1" : "Apple Watch 38mm case", - "Watch1,2" : "Apple Watch 42mm case", - "Watch2,6" : "Apple Watch Series 1 38mm case", - "Watch2,7" : "Apple Watch Series 1 42mm case", - "Watch2,3" : "Apple Watch Series 2 38mm case", - "Watch2,4" : "Apple Watch Series 2 42mm case", - "Watch3,1" : "Apple Watch Series 3 38mm case (GPS+Cellular)", - "Watch3,2" : "Apple Watch Series 3 42mm case (GPS+Cellular)", - "Watch3,3" : "Apple Watch Series 3 38mm case (GPS)", - "Watch3,4" : "Apple Watch Series 3 42mm case (GPS)", - "Watch4,1" : "Apple Watch Series 4 40mm case (GPS)", - "Watch4,2" : "Apple Watch Series 4 44mm case (GPS)", - "Watch4,3" : "Apple Watch Series 4 40mm case (GPS+Cellular)", - "Watch4,4" : "Apple Watch Series 4 44mm case (GPS+Cellular)", - "Watch5,1" : "Apple Watch Series 5 40mm case (GPS)", - "Watch5,2" : "Apple Watch Series 5 44mm case (GPS)", - "Watch5,3" : "Apple Watch Series 5 40mm case (GPS+Cellular)", - "Watch5,4" : "Apple Watch Series 5 44mm case (GPS+Cellular)", - "Watch5,9" : "Apple Watch SE 40mm case (GPS)", - "Watch5,10" : "Apple Watch SE 44mm case (GPS)", - "Watch5,11" : "Apple Watch SE 40mm case (GPS+Cellular)", - "Watch5,12" : "Apple Watch SE 44mm case (GPS+Cellular)", - "Watch6,1" : "Apple Watch Series 6 40mm case (GPS)", - "Watch6,2" : "Apple Watch Series 6 44mm case (GPS)", - "Watch6,3" : "Apple Watch Series 6 40mm case (GPS+Cellular)", - "Watch6,4" : "Apple Watch Series 6 44mm case (GPS+Cellular)", - "Watch6,6" : "Apple Watch Series 7 41mm case (GPS)", - "Watch6,7" : "Apple Watch Series 7 45mm case (GPS)", - "Watch6,8" : "Apple Watch Series 7 41mm case (GPS+Cellular)", - "Watch6,9" : "Apple Watch Series 7 45mm case (GPS+Cellular)", - "Watch6,10" : "Apple Watch SE 40mm case (GPS)", - "Watch6,11" : "Apple Watch SE 44mm case (GPS)", - "Watch6,12" : "Apple Watch SE 40mm case (GPS+Cellular)", - "Watch6,13" : "Apple Watch SE 44mm case (GPS+Cellular)", - "Watch6,14" : "Apple Watch Series 8 41mm case (GPS)", - "Watch6,15" : "Apple Watch Series 8 45mm case (GPS)", - "Watch6,16" : "Apple Watch Series 8 41mm case (GPS+Cellular)", - "Watch6,17" : "Apple Watch Series 8 45mm case (GPS+Cellular)", - "Watch6,18" : "Apple Watch Ultra", - "Watch7,1" : "Apple Watch Series 9 41mm case (GPS)", - "Watch7,2" : "Apple Watch Series 9 45mm case (GPS)", - "Watch7,3" : "Apple Watch Series 9 41mm case (GPS+Cellular)", - "Watch7,4" : "Apple Watch Series 9 45mm case (GPS+Cellular)", - "Watch7,5" : "Apple Watch Ultra 2", -] diff --git a/HealthImport/Model/Tables/DataSeriesTable.swift b/HealthImport/Model/Tables/DataSeriesTable.swift deleted file mode 100644 index a6407ee..0000000 --- a/HealthImport/Model/Tables/DataSeriesTable.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import SQLite - -struct DataSeriesTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("data_series") -} diff --git a/HealthImport/Model/Tables/LocationSeriesDataTable.swift b/HealthImport/Model/Tables/LocationSeriesDataTable.swift deleted file mode 100644 index 9276dba..0000000 --- a/HealthImport/Model/Tables/LocationSeriesDataTable.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation -import SQLite -import CoreLocation - -typealias LocationSample = CLLocation - -struct LocationSeriesDataTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("location_series_data") - - /// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]` - let seriesIdentifier = Expression("series_identifier") - - let timestamp = Expression("timestamp") - - let longitude = Expression("longitude") - - let latitude = Expression("latitude") - - let altitude = Expression("altitude") - - let speed = Expression("speed") - - let course = Expression("course") - - let horizontalAccuracy = Expression("horizontal_accuracy") - - let verticalAccuracy = Expression("vertical_accuracy") - - let speedAccuracy = Expression("speed_accuracy") - - let courseAccuracy = Expression("course_accuracy") - - let signalEnvironment = Expression("signal_environment") - - func locationSamples(for seriesId: Int) throws -> [LocationSample] { - try database.prepare(table.filter(seriesIdentifier == seriesId)).map(location) - } - - func locationSampleCount(for seriesId: Int) throws -> Int { - try database.scalar(table.filter(seriesIdentifier == seriesId).count) - } - - func locationSamples(from start: Date, to end: Date) throws -> [LocationSample] { - let startTime = start.timeIntervalSinceReferenceDate - let endTime = end.timeIntervalSinceReferenceDate - return try database.prepare(table.filter(timestamp >= startTime && timestamp <= endTime)).map(location) - } - - func locationSampleCount(from start: Date, to end: Date) throws -> Int { - let startTime = start.timeIntervalSinceReferenceDate - let endTime = end.timeIntervalSinceReferenceDate - return try database.scalar(table.filter(timestamp >= startTime && timestamp <= endTime).count) - } - - func location(row: Row) -> LocationSample { - .init( - coordinate: .init( - latitude: row[latitude], - longitude: row[longitude]), - altitude: row[altitude], - horizontalAccuracy: row[horizontalAccuracy], - verticalAccuracy: row[horizontalAccuracy], - course: row[course], - courseAccuracy: row[courseAccuracy], - speed: row[speed], - speedAccuracy: row[speedAccuracy], - timestamp: .init(timeIntervalSinceReferenceDate: row[timestamp]), - sourceInfo: .init()) - } - - func create(references dataSeries: DataSeriesTable) throws { - try database.execute("CREATE TABLE location_series_data (series_identifier INTEGER NOT NULL REFERENCES data_series(hfd_key) DEFERRABLE INITIALLY DEFERRED, timestamp REAL NOT NULL, longitude REAL NOT NULL, latitude REAL NOT NULL, altitude REAL NOT NULL, speed REAL NOT NULL, course REAL NOT NULL, horizontal_accuracy REAL NOT NULL, vertical_accuracy REAL NOT NULL, speed_accuracy REAL NOT NULL, course_accuracy REAL NOT NULL, signal_environment INTEGER NOT NULL, PRIMARY KEY (series_identifier, timestamp)) WITHOUT ROWID") - } - - func insert(_ sample: LocationSample, seriesId: Int) throws { - try database.run(table.insert( - seriesIdentifier <- seriesId, - timestamp <- sample.timestamp.timeIntervalSinceReferenceDate, - longitude <- sample.coordinate.longitude, - latitude <- sample.coordinate.latitude, - altitude <- sample.altitude, - speed <- sample.speed, - course <- sample.course, - horizontalAccuracy <- sample.horizontalAccuracy, - horizontalAccuracy <- sample.verticalAccuracy, - speedAccuracy <- sample.speedAccuracy, - courseAccuracy <- sample.courseAccuracy, - signalEnvironment <- 1 - )) - } -} diff --git a/HealthImport/Model/Tables/MetadataKeysTable.swift b/HealthImport/Model/Tables/MetadataKeysTable.swift deleted file mode 100644 index e363cdb..0000000 --- a/HealthImport/Model/Tables/MetadataKeysTable.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import SQLite - -struct MetadataKeysTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("metadata_keys") - - let rowId = Expression("ROWID") - - let key = Expression("key") - - func key(for keyId: Int, in database: Connection) throws -> String { - try database.pluck(table.filter(rowId == keyId)).map { $0[key] }! - } - - func all() throws -> [Int : Metadata.Key] { - try database.prepare(table).reduce(into: [:]) { dict, row in - dict[row[rowId]] = .init(rawValue: row[key]) - } - } - - func create() throws { - //try database.execute("CREATE TABLE metadata_keys (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE)") - try database.run(table.create { table in - table.column(rowId, primaryKey: .autoincrement) - table.column(key, unique: true) - }) - } - - func hasKey(_ key: Metadata.Key) throws -> Int? { - try database.pluck(table.filter(self.key == key.rawValue)).map { $0[rowId] } - } - - func insert(key: Metadata.Key) throws -> Int { - Int(try database.run(table.insert(self.key <- key.rawValue))) - } - -} diff --git a/HealthImport/Model/Tables/MetadataTables.swift b/HealthImport/Model/Tables/MetadataTables.swift deleted file mode 100644 index e12b2d4..0000000 --- a/HealthImport/Model/Tables/MetadataTables.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import SQLite - -struct MetadataTables { - - private let database: Connection - - let values: MetadataValuesTable - - let keys: MetadataKeysTable - - init(database: Connection) { - self.database = database - self.keys = .init(database: database) - self.values = .init(database: database) - } - - func create() throws { - try values.create() - try keys.create() - } - - func metadata(for workoutId: Int) throws -> [Metadata.Key : Metadata.Value] { - // Keys: rowId -> String - - let selection = values.table - .select(values.table[*], keys.table[keys.key]) - .filter(values.objectId == workoutId) - .join(.leftOuter, keys.table, on: values.table[values.keyId] == keys.table[keys.rowId]) - - return try database.prepare(selection).reduce(into: [:]) { dict, row in - let key = Metadata.Key(rawValue: row[keys.key]) - let value = values.from(row: row) - dict[key] = value - } - } - - func insert(_ value: Metadata.Value, for key: Metadata.Key, of workoutId: Int) throws { - let keyId = try keys.hasKey(key) ?? keys.insert(key: key) - try values.insert(value, of: workoutId, for: keyId) - } -} diff --git a/HealthImport/Model/Tables/MetadataValuesTable.swift b/HealthImport/Model/Tables/MetadataValuesTable.swift deleted file mode 100644 index 4c8d8dc..0000000 --- a/HealthImport/Model/Tables/MetadataValuesTable.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation -import SQLite - -struct MetadataValuesTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("metadata_values") - - let rowId = Expression("ROW_ID") - - let keyId = Expression("key_id") - - let objectId = Expression("object_id") - - let valueType = Expression("value_type") - - let stringValue = Expression("string_value") - - let numericalValue = Expression("numerical_value") - - let dateValue = Expression("date_value") - - let dataValue = Expression("data_value") - - func all() throws -> [Metadata.Value] { - try database.prepare(table).map(from) - } - - func metadata(for workoutId: Int) throws -> [Metadata.Value] { - try database.prepare(table.filter(objectId == workoutId)).map(from) - } - - func metadata(for workoutId: Int) throws -> [(keyId: Int, value: Metadata.Value)] { - try database.prepare(table.filter(objectId == workoutId)).compactMap { row in - guard let keyId = row[keyId] else { - print("Found 'key_id == NULL' for metadata value of workout \(workoutId)") - return nil - } - return (keyId, from(row: row)) - } - } - - func from(row: Row) -> Metadata.Value { - let valueType = Metadata.Value.ValueType(rawValue: row[valueType])! - switch valueType { - case .string: - return .string(value: row[stringValue]!) - case .number: - return .number(value: row[numericalValue]!) - case .date: - return .date(value: .init(timeIntervalSinceReferenceDate: row[dateValue]!)) - case .numerical: - return .numerical(value: row[numericalValue]!, unit: row[stringValue]!) - case .data: - return .data(value: row[dataValue]!) - } - } - - func create() throws { - //try database.execute("CREATE TABLE metadata_values (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key_id INTEGER, object_id INTEGER, value_type INTEGER NOT NULL DEFAULT 0, string_value TEXT, numerical_value REAL, date_value REAL, data_value BLOB)") - try database.run(table.create { table in - table.column(rowId, primaryKey: .autoincrement) - table.column(keyId) - table.column(objectId) - table.column(valueType, defaultValue: 0) - table.column(stringValue) - table.column(numericalValue) - table.column(dateValue) - table.column(dataValue) - }) - } - - func insert(_ element: Metadata.Value, of workoutId: Int, for keyId: Int) throws { - try database.run(table.insert( - self.keyId <- keyId, - objectId <- workoutId, - valueType <- element.valueType.rawValue, - stringValue <- element.stringValue, - numericalValue <- element.numericalValue, - dateValue <- element.dateValue, - dataValue <- element.dataValue - )) - } -} - -private extension Metadata.Value { - - var stringValue: String? { - if case let .string(value) = self { - return value - } - if case let .numerical(_, unit) = self { - return unit - } - return nil - } - - var numericalValue: Double? { - if case let .number(value) = self { - return value - } - if case let .numerical(value: value, unit: _) = self { - return value - } - return nil - } - - var dateValue: Double? { - if case let .date(value: date) = self { - return date.timeIntervalSinceReferenceDate - } - return nil - } - - var dataValue: Data? { - if case let .data(data) = self { - return data - } - return nil - } -} diff --git a/HealthImport/Model/Tables/ObjectsTable.swift b/HealthImport/Model/Tables/ObjectsTable.swift deleted file mode 100644 index e7332eb..0000000 --- a/HealthImport/Model/Tables/ObjectsTable.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import SQLite - -struct ObjectsTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - func create(referencing dataProvenances: DataProvenancesTable) throws { - try database.execute("CREATE TABLE objects (data_id INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE, provenance INTEGER NOT NULL REFERENCES data_provenances (ROWID) ON DELETE CASCADE, type INTEGER, creation_date REAL)") - } - - let table = Table("objects") - - let dataId = Expression("data_id") - - let uuid = Expression("uuid") - - let provenance = Expression("provenance") - - let type = Expression("type") - - let creationDate = Expression("creation_date") - - func object(for dataId: Int) throws -> (uuid: UUID, provenance: Int, type: Int, creationDate: Date)? { - try database.pluck(table.filter(self.dataId == dataId)).map { row in - let uuid = row[uuid]!.asUUID()! - let provenance = row[provenance] - let type = row[type]! - let creationDate = Date(timeIntervalSinceReferenceDate: row[creationDate]!) - return (uuid, provenance, type, creationDate) - } - } -} diff --git a/HealthImport/Model/Tables/QuantitySamplesTable.swift b/HealthImport/Model/Tables/QuantitySamplesTable.swift deleted file mode 100644 index 564d15f..0000000 --- a/HealthImport/Model/Tables/QuantitySamplesTable.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SQLite - -struct QuantitySamplesTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - func create(referencing unitStrings: UnitStringsTable) throws { - try database.execute("CREATE TABLE quantity_samples (data_id INTEGER PRIMARY KEY, quantity REAL, original_quantity REAL, original_unit INTEGER REFERENCES unit_strings (ROWID) ON DELETE NO ACTION)") - } - - let table = Table("quantity_samples") - - let dataId = Expression("data_id") - - let quantity = Expression("quantity") - - let originalQuantity = Expression("original_quantity") - - /// References `ROWID` on table `unit_strings` - let originalUnit = Expression("original_unit") - - func quantity(for id: Int, in database: Database) throws -> (quantity: Double?, original: Double?, unit: Int?) { - try database.prepare(table.filter(dataId == id).limit(1)).map { - (quantity: $0[quantity], original: $0[originalQuantity], unit: $0[originalUnit]) - }.first ?? (nil, nil, nil) - } -} diff --git a/HealthImport/Model/Tables/SamplesTable.swift b/HealthImport/Model/Tables/SamplesTable.swift deleted file mode 100644 index fb665e8..0000000 --- a/HealthImport/Model/Tables/SamplesTable.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation -import SQLite - -struct SamplesTable { - - private let database: Connection - - private let quantitySamples: QuantitySamplesTable - - private let objects: ObjectsTable - - private let dataProvenances: DataProvenancesTable - - private let unitStrings: UnitStringsTable - - init(database: Connection) { - self.database = database - self.quantitySamples = .init(database: database) - self.objects = .init(database: database) - self.dataProvenances = .init(database: database) - self.unitStrings = .init(database: database) - } - - func create() throws { - try database.execute("CREATE TABLE samples (data_id INTEGER PRIMARY KEY, start_date REAL, end_date REAL, data_type INTEGER)") - } - - func createAll() throws { - try create() - try unitStrings.create() - try quantitySamples.create(referencing: unitStrings) - try dataProvenances.create() - try objects.create(referencing: dataProvenances) - } - - private let table = Table("samples") - - private let dataId = Expression("data_id") - - // NOTE: Technically optional - private let startDate = Expression("start_date") - - // NOTE: Technically optional - private let endDate = Expression("end_date") - - private let dataType = Expression("data_type") - - func samples(from start: Date, to end: Date) throws -> [Sample] { - let start = start.timeIntervalSinceReferenceDate - let end = end.timeIntervalSinceReferenceDate - - // Samples: data_id, start_date, end_date, data_type - // JOIN quantity_samples on samples.data_id == quantity_samples.data_id - // quantity_samples: quantity, original_quantity, original_unit - // JOIN objects on samples.data_id == objects.data_id - // objects: data_id, uuid, provenance, type, creation_date - - // JOIN data_provenances on objects.provenance == data_provenances.ROWID - // SELECT tz_name FROM data_provenances - - let selection = table - .select(table[*], - quantitySamples.table[*], - dataProvenances.table[dataProvenances.tzName], - unitStrings.table[unitStrings.unitString]) - .filter(startDate >= start && endDate <= end) - .join(.leftOuter, quantitySamples.table, on: table[dataId] == quantitySamples.table[quantitySamples.dataId]) - .join(.leftOuter, objects.table, on: table[dataId] == objects.table[objects.dataId]) - .join(.leftOuter, dataProvenances.table, on: objects.table[objects.provenance] == dataProvenances.table[dataProvenances.rowId]) - .join(.leftOuter, unitStrings.table, on: quantitySamples.table[quantitySamples.originalUnit] == unitStrings.table[unitStrings.rowId]) - - return try database.prepare(selection).map { row in - let startDate = Date(timeIntervalSinceReferenceDate: row[startDate]) - let endDate = Date(timeIntervalSinceReferenceDate: row[endDate]) - let dataType = Sample.DataType(rawValue: row[dataType]) - let quantity = row[quantitySamples.quantity] - let original = row[quantitySamples.originalQuantity] - let unit = row[unitStrings.unitString] - let timeZone = row[dataProvenances.tzName].nonEmpty - return .init( - startDate: startDate, - endDate: endDate, - dataType: dataType, - quantity: quantity, - originalQuantity: original, - originalUnit: unit, - timeZoneName: timeZone) - } - } - - func sampleCount(from start: Date, to end: Date) throws -> Int { - let start = start.timeIntervalSinceReferenceDate - let end = end.timeIntervalSinceReferenceDate - return try database.scalar(table.filter(startDate >= start && endDate <= end).count) - } -} diff --git a/HealthImport/Model/Tables/UnitStringsTable.swift b/HealthImport/Model/Tables/UnitStringsTable.swift deleted file mode 100644 index c2ada22..0000000 --- a/HealthImport/Model/Tables/UnitStringsTable.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import SQLite - -struct UnitStringsTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - func create() throws { - try database.execute("CREATE TABLE unit_strings (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, unit_string TEXT UNIQUE)") - } - - let table = Table("unit_strings") - - let rowId = Expression("ROWID") - - let unitString = Expression("unit_string") - - func unit(for id: Int) throws -> String? { - try database.pluck(table.filter(rowId == id).limit(1)).map { row in - row[unitString] - } - } -} diff --git a/HealthImport/Model/Tables/WorkoutActivitiesTable.swift b/HealthImport/Model/Tables/WorkoutActivitiesTable.swift deleted file mode 100644 index 42260a1..0000000 --- a/HealthImport/Model/Tables/WorkoutActivitiesTable.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import SQLite -import HealthKit - -struct WorkoutActivitiesTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("workout_activities") - - let rowId = Expression("ROWID") - - let uuid = Expression("uuid") - - let ownerId = Expression("owner_id") - - let isPrimaryActivity = Expression("is_primary_activity") - - let activityType = Expression("activity_type") - - let locationType = Expression("location_type") - - let swimmingLocationType = Expression("swimming_location_type") - - let lapLength = Expression("lap_length") - - let startDate = Expression("start_date") - - let endDate = Expression("end_date") - - let duration = Expression("duration") - - let metadata = Expression("metadata") - - func activities() throws -> [HKWorkoutActivity] { - try database.prepare(table).map(activity) - } - - func activity(from row: Row) throws -> HKWorkoutActivity { - let configuration = HKWorkoutConfiguration() - configuration.lapLength = try row[lapLength].map(WorkoutActivitiesTable.lapLength) - configuration.activityType = .init(rawValue: UInt(row[activityType]))! - configuration.locationType = .init(rawValue: row[locationType])! - configuration.swimmingLocationType = .init(rawValue: row[swimmingLocationType])! - - let start = Date(timeIntervalSinceReferenceDate: row[startDate]) - let end = Date(timeIntervalSinceReferenceDate: row[endDate]) - let uuid = row[uuid].uuidString - - var metadata: [String : Any] = [ : ] - metadata[HKMetadataKeyExternalUUID] = uuid - - // duration: row[columnDuration] - // isPrimaryActivity: row[columnIsPrimaryActivity] - - // metadata: row[columnMetadata] - // TODO: Decode activity metadata - return .init( - workoutConfiguration: configuration, - start: start, - end: end, - metadata: metadata) - } - - func activities(for workoutId: Int) throws -> [HKWorkoutActivity] { - try database.prepare(table.filter(ownerId == workoutId)).map(activity) - } - - func create(referencing workouts: WorkoutsTable) throws { - try database.execute("CREATE TABLE workout_activities (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE NOT NULL, owner_id INTEGER NOT NULL REFERENCES workouts(data_id) ON DELETE CASCADE, is_primary_activity INTEGER NOT NULL, activity_type INTEGER NOT NULL, location_type INTEGER NOT NULL, swimming_location_type INTEGER NOT NULL, lap_length BLOB, start_date REAL NOT NULL, end_date REAL NOT NULL, duration REAL NOT NULL, metadata BLOB)") - /* - try database.run(table.create { t in - t.column(rowId, primaryKey: .autoincrement) - t.column(uuid, unique: true) - t.column(ownerId, references: workouts.table, workouts.dataId) // TODO: ON DELETE CASCADE - t.column(isPrimaryActivity) - t.column(activityType) - t.column(locationType) - t.column(swimmingLocationType) - t.column(lapLength) - t.column(startDate) - t.column(endDate) - t.column(duration) - t.column(metadata) - }) - */ - } - - func insert(_ element: HKWorkoutActivity, isPrimaryActivity: Bool, dataId: Int) throws { - try database.run(table.insert( - uuid <- (element.externalUUID ?? element.uuid).uuidString.data(using: .utf8)!, - ownerId <- dataId, - self.isPrimaryActivity <- isPrimaryActivity, // Seems to always be 1 - activityType <- Int(element.workoutConfiguration.activityType.rawValue), - locationType <- element.workoutConfiguration.locationType.rawValue, - swimmingLocationType <- element.workoutConfiguration.swimmingLocationType.rawValue, - lapLength <- try WorkoutActivitiesTable.lapLengthData(lapLength: element.workoutConfiguration.lapLength), - startDate <- element.startDate.timeIntervalSinceReferenceDate, - endDate <- element.endDate?.timeIntervalSinceReferenceDate ?? element.startDate.addingTimeInterval(element.duration).timeIntervalSinceReferenceDate, - duration <- element.duration, - metadata <- nil) - ) - } -} - -private extension WorkoutActivitiesTable { - - static func lapLengthData(lapLength: HKQuantity?) throws -> Data? { - try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) } - } - - static func lapLength(from data: Data) throws -> HKQuantity? { - try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data) - } -} diff --git a/HealthImport/Model/Tables/WorkoutEventsTable.swift b/HealthImport/Model/Tables/WorkoutEventsTable.swift deleted file mode 100644 index c588a3e..0000000 --- a/HealthImport/Model/Tables/WorkoutEventsTable.swift +++ /dev/null @@ -1,174 +0,0 @@ -import Foundation -import SQLite -import HealthKit - -struct WorkoutEventsTable { - - private let database: Connection - - init(database: Connection) { - self.database = database - } - - let table = Table("workout_events") - - // INTEGER PRIMARY KEY AUTOINCREMENT - let rowId = Expression("ROWID") - - // owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE - let ownerId = Expression("owner_id") - - // date REAL NOT NULL - let date = Expression("date") - - // type INTEGER NOT NULL - let type = Expression("type") - - // duration REAL NOT NULL - let duration = Expression("duration") - - // metadata BLOB - let metadata = Expression("metadata") - - // session_uuid BLOB - let sessionUUID = Expression("session_uuid") - - // error BLOB - let error = Expression("error") - - func events(in database: Connection) throws -> [HKWorkoutEvent] { - try database.prepare(table).map(event) - } - - func events(for workoutId: Int, in database: Connection) throws -> [HKWorkoutEvent] { - try database.prepare(table.filter(ownerId == workoutId)).map(event) - } - - private func event(from row: Row) -> HKWorkoutEvent { - let start = Date(timeIntervalSinceReferenceDate: row[date]) - let interval = DateInterval(start: start, duration: row[duration]) - let metadata = metadata(row[metadata]) - let type = HKWorkoutEventType(rawValue: row[type])! - // let sessionUUID = row[sessionUUID] - // let error = row[rrror] - return .init(type: type, dateInterval: interval, metadata: metadata) - } - - private func metadata(_ data: Data?) -> [String : Any] { - guard let data else { - return [:] - } - return WorkoutEventsTable.decode(metadata: data) - } - - func create(referencing workouts: WorkoutsTable) throws { - // try database.execute("CREATE TABLE workout_events (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE, date REAL NOT NULL, type INTEGER NOT NULL, duration REAL NOT NULL, metadata BLOB, session_uuid BLOB, error BLOB)") - try database.run(table.create { t in - t.column(rowId, primaryKey: .autoincrement) - t.column(ownerId, references: workouts.table, workouts.dataId) - t.column(date) - t.column(type) - t.column(duration) - t.column(metadata) - t.column(sessionUUID) - t.column(error) - }) - } - - func insert(_ element: HKWorkoutEvent, dataId: Int) throws { - try database.run(table.insert( - ownerId <- dataId, - date <- element.dateInterval.start.timeIntervalSinceReferenceDate, - type <- element.type.rawValue, - duration <- element.dateInterval.duration, - metadata <- WorkoutEventsTable.encode(metadata: element.metadata ?? [:])) - // SessionUUID <- element.sessionUUID - // Error <- element.error) - ) - } -} - -extension WorkoutEventsTable { - - static func decode(metadata data: Data) -> [String : Any] { - let metadata: WorkoutEventMetadata - do { - metadata = try WorkoutEventMetadata(serializedData: data) - } catch { - print("Failed to decode event metadata: \(error)") - print(data.hex) - return [:] - } - - return metadata.elements.reduce(into: [:]) { dict, element in - guard let value = element.value else { - print("No value for metadata element \(element)") - print(data.hex) - return - } - dict[element.key] = value - } - } - - static func encode(metadata: [String : Any]) -> Data? { - let wrapper = WorkoutEventMetadata.with { - $0.elements = metadata.compactMap { .from(key: $0.key, value: $0.value) } - } - guard !wrapper.elements.isEmpty else { - return nil - } - do { - return try wrapper.serializedData() - } catch { - print("Failed to encode event metadata: \(error)") - return nil - } - } -} - -private extension WorkoutEventMetadata.Element { - - var value: Any? { - if hasUnsignedValue { - return unsignedValue - } - if hasQuantity { - return HKQuantity(unit: .init(from: quantity.unit), doubleValue: quantity.value) - } - return UInt(0) - } - - static func from(key: String, value: Any) -> Self? { - if let value = value as? UInt64 { - return .with { - $0.key = key - $0.unsignedValue = UInt64(value) - } - } - guard let value = value as? HKQuantity else { - print("Unknown value type for metadata key \(key): \(value)") - return nil - } - - let number: Double - let unit: String - if value.is(compatibleWith: .meter()) { - number = value.doubleValue(for: .meter()) - unit = "m" - } else if value.is(compatibleWith: .second()) { - number = value.doubleValue(for: .second()) - unit = "s" - } else { - print("Unhandled quantity type for metadata key \(key): \(value)") - return nil - } - - return .with { el in - el.key = key - el.quantity = .with { - $0.value = number - $0.unit = unit - } - } - } -} diff --git a/HealthImport/Model/Tables/WorkoutsTable.swift b/HealthImport/Model/Tables/WorkoutsTable.swift deleted file mode 100644 index 7206c62..0000000 --- a/HealthImport/Model/Tables/WorkoutsTable.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import SQLite -import HealthKit - -struct WorkoutsTable { - - private let database: Connection - - let events: WorkoutEventsTable - - let activities: WorkoutActivitiesTable - - let metadata: MetadataTables - - init(database: Connection) { - self.database = database - self.events = .init(database: database) - self.activities = .init(database: database) - self.metadata = .init(database: database) - } - - let table = Table("workouts") - - // INTEGER PRIMARY KEY AUTOINCREMENT - let dataId = Expression("data_id") - - // REAL - let totalDistance = Expression("total_distance") - - // INTEGER - let goalType = Expression("goal_type") - - // REAL - let goal = Expression("goal") - - // INTEGER - let condenserVersion = Expression("condenser_version") - - // REAL - let condenserDate = Expression("condenser_date") - - func workouts() throws -> [Workout] { - return try database.prepare(table).map { row in - let id = row[dataId] - - let events = try events.events(for: id, in: database) - let activities = try activities.activities(for: id) - let metadata = try metadata.metadata(for: id) - return .init( - id: id, - totalDistance: row[totalDistance], - goalType: row[goalType], - goal: row[goal], - condenserVersion: row[condenserVersion], - condenserDate: row[condenserDate].map { Date.init(timeIntervalSinceReferenceDate: $0) }, - events: events, - activities: activities, - metadata: metadata) - } - } - - func create() throws { - try database.run(table.create { t in - t.column(dataId, primaryKey: .autoincrement) - t.column(totalDistance) - t.column(goalType) - t.column(goal) - t.column(condenserVersion) - t.column(condenserDate) - }) - // try database.execute("CREATE TABLE workouts (data_id INTEGER PRIMARY KEY AUTOINCREMENT, total_distance REAL, goal_type INTEGER, goal REAL, condenser_version INTEGER, condenser_date REAL)") - } - - func createAll() throws { - try create() - try events.create(referencing: self) - try activities.create(referencing: self) - } - - func insert(_ element: Workout) throws { - let rowid = try database.run(table.insert( - totalDistance <- element.totalDistance, - goalType <- element.goal?.goalType, - goal <- element.goal?.gaol, - condenserVersion <- element.condenserVersion, - condenserDate <- element.condenserDate?.timeIntervalSinceReferenceDate) - ) - let dataId = Int(rowid) - for event in element.events { - try events.insert(event, dataId: dataId) - } - - for activity in element.activities { - try activities.insert(activity, isPrimaryActivity: true, dataId: dataId) - } - - for (key, value) in element.metadata { - try metadata.insert(value, for: key, of: dataId) - } - } -} - diff --git a/HealthImport/Model/Workout.swift b/HealthImport/Model/Workout.swift deleted file mode 100644 index ed02171..0000000 --- a/HealthImport/Model/Workout.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation -import Collections -import HealthKit - -private let df: DateFormatter = { - let df = DateFormatter() - df.timeZone = .current - df.dateStyle = .short - df.timeStyle = .short - return df -}() - -struct Workout { - - let id: Int - - /// The distance in km (?) - let totalDistance: Double? - - let goal: Goal? - - let condenserVersion: Int? - - let condenserDate: Date? - - let events: [HKWorkoutEvent] - - let activities: [HKWorkoutActivity] - - let metadata: OrderedDictionary - - var firstActivityDate: Date? { - activities.map { $0.startDate }.min() - } - - var firstEventDate: Date? { - events.map { $0.dateInterval.start }.min() - } - - var firstAvailableDate: Date? { - [condenserDate, firstEventDate, firstActivityDate].compactMap { $0 }.min() - } - - var dateString: String { - guard let firstAvailableDate else { - return "No date" - } - return df.string(from: firstAvailableDate) - } - - var typeString: String { - activities.first?.workoutConfiguration.activityType.description ?? "Unknown activity" - } - - init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [HKWorkoutEvent] = [], activities: [HKWorkoutActivity] = [], metadata: [Metadata.Key : Metadata.Value] = [:]) { - self.id = id - self.totalDistance = totalDistance - self.goal = .init(goalType: goalType, goal: goal) - self.condenserVersion = condenserVersion - self.condenserDate = condenserDate - self.events = events - self.activities = activities - self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values) - } -} - -extension Workout: Identifiable { } - -extension Workout: Equatable { - static func == (lhs: Workout, rhs: Workout) -> Bool { - lhs.id == rhs.id - } -} - -extension Workout: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} diff --git a/HealthImport/Preview Content/HealthDatabase+Mock.swift b/HealthImport/Preview Content/HealthDatabase+Mock.swift index 822e235..e8da73b 100644 --- a/HealthImport/Preview Content/HealthDatabase+Mock.swift +++ b/HealthImport/Preview Content/HealthDatabase+Mock.swift @@ -1,6 +1,7 @@ import Foundation import SQLite import HealthKit +import HKDatabase extension HealthDatabase { diff --git a/HealthImport/Preview Content/Location+Mock.swift b/HealthImport/Preview Content/Location+Mock.swift index 23412e5..bfea1df 100644 --- a/HealthImport/Preview Content/Location+Mock.swift +++ b/HealthImport/Preview Content/Location+Mock.swift @@ -1,9 +1,9 @@ import Foundation import CoreLocation -extension LocationSample { +extension CLLocation { - static let mock: LocationSample = .init( + static let mock: CLLocation = .init( coordinate: .init(latitude: 52.27124117, longitude: 10.53865853), altitude: 2340, // m horizontalAccuracy: 3.5, // m diff --git a/HealthImport/Preview Content/Metadata+Mock.swift b/HealthImport/Preview Content/Metadata+Mock.swift index 014d6d8..9213131 100644 --- a/HealthImport/Preview Content/Metadata+Mock.swift +++ b/HealthImport/Preview Content/Metadata+Mock.swift @@ -1,34 +1,37 @@ import Foundation +import HKDatabase +import HealthKitExtensions +import HealthKit extension Metadata { - static let mock1: [Key : Value] = [ - .indoorWorkout : .number(value: 0.0), - .elevationAscended : .numerical(value: 100564.0, unit: "cm"), - .timeZone : .string(value: "Europe/Berlin"), - .weatherHumidity : .numerical(value: 84.0, unit: "%"), - .weatherTemperature : .numerical(value: 20.8219999999961, unit: "degF"), - .averageMETs : .numerical(value: 4.12470993502559, unit: "kcal/hr·kg"), - .privateWeatherCondition : .number(value: 4.0), - .privateWorkoutActivityMoveMode : .number(value: 1.0), - .privateWorkoutMaxGroundElevation : .numerical(value: 3188.78609748806, unit: "m"), - .privateWorkoutWeatherSourceName : .string(value: "Apple Weather"), - .privateWorkoutHeartRateZones : .data(value: .init(hex: "62706c6973743030a5010a121a22d402030405060708095f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657810052300000000000000002340604000000000001000d40b0c0d0e060f10115f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e6465782340604000000000002340620000000000001001d413141516061718195f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234062000000000000234063c000000000001002d41b1c1d1e061f20215f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234063c000000000002340658000000000001003d423242526062728295f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657823406580000000000023406740000000000010040008000e0017002c004000540069006b0074007d007f0088009d00b100c500da00e300ec00ee00f7010c0120013401490152015b015d0166017b018f01a301b801c101ca01cc01d501ea01fe02120227023002390000000000000201000000000000002a0000000000000000000000000000023b")!), - .privateWorkoutMinHeartRate : .numerical(value: 1.05, unit: "count/s"), - .privateMetricPlatterStatistics : .string(value: """ + static let mock1: [String : Any] = [ + HKMetadataKeyIndoorWorkout : NSNumber(0.0), + HKMetadataKeyElevationAscended : HKQuantity(unit: .meterUnit(with: .centi), doubleValue: 100564.0), + HKMetadataKeyTimeZone : "Europe/Berlin", + HKMetadataKeyWeatherHumidity : HKQuantity(unit: .percent(), doubleValue: 84.0), + HKMetadataKeyWeatherTemperature : HKQuantity(unit: .degreeCelsius(), doubleValue: 20.8219999999961), + HKMetadataKeyAverageMETs : HKQuantity(unit: .init(from: "kcal/hr*kg"), doubleValue: 4.12470993502559), + "_HKPrivateWeatherCondition" : NSNumber(4.0), + "_HKPrivateWorkoutActivityMoveMode" : NSNumber(1.0), + "_HKPrivateWorkoutMaxGroundElevation" : HKQuantity(unit: .meter(), doubleValue: 3188.78609748806), + "_HKPrivateWorkoutWeatherSourceName" : "Apple Weather", + "_HKPrivateWorkoutHeartRateZones" : Data(hex: "62706c6973743030a5010a121a22d402030405060708095f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657810052300000000000000002340604000000000001000d40b0c0d0e060f10115f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e6465782340604000000000002340620000000000001001d413141516061718195f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234062000000000000234063c000000000001002d41b1c1d1e061f20215f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234063c000000000002340658000000000001003d423242526062728295f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657823406580000000000023406740000000000010040008000e0017002c004000540069006b0074007d007f0088009d00b100c500da00e300ec00ee00f7010c0120013401490152015b015d0166017b018f01a301b801c101ca01cc01d501ea01fe02120227023002390000000000000201000000000000002a0000000000000000000000000000023b")!, + "_HKPrivateWorkoutMinHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 1.05), + "_HKPrivateMetadataKeyMetricPlatterStatistics" : """ {"currentMetricPlatterType":"standard","currentMetricPlatterElapsedTime":17991.661971926689,"metricPlatterAccumulatedTime":{"standard":5039.4511208534241,"activityRings":31.905926942825317,"elevation":12920.30492413044}} -"""), - .privateWorkoutWasInDaytime : .number(value: 1.0), - .privateWorkoutWeatherLocationCoordinatesLatitude : .number(value: 47.0850386887433), - .privateWorkoutMinGroundElevation : .numerical(value: 2147.15929389872, unit: "m"), - .privateWorkoutConfiguration : .data(value: """ +""", + "_HKPrivateWorkoutWasInDaytime" : NSNumber(1.0), + "_HKPrivateWorkoutWeatherLocationCoordinatesLatitude" : NSNumber(47.0850386887433), + "_HKPrivateWorkoutMinGroundElevation" : HKQuantity(unit: .meter(), doubleValue: 2147.15929389872), + "_HKPrivateWorkoutConfiguration" : """ {"strings":{},"type":2,"data":"eyJ0eXBlIjoxLCJnb2FsIjoiWW5Cc2FYTjBNRERVQVFJREJBVXNMUzVZSkc5aWFtVmpkSE5ZSkhabGNuTnBiMjVaSkdGeVkyaHBkbVZ5VkNSMGIzQ29CZ2NRRmhvYkpDaFZKRzUxYkd6VUNBa0tDd3dORGc5ZkVCMU9URk5sYzNOcGIyNUJZM1JwZG1sMGVVZHZZV3hSZFdGdWRHbDBlVjhRSjA1TVUyVnpjMmx2YmtGamRHbDJhWFI1UjI5aGJFZHZZV3hVZVhCbFNXUmxiblJwWm1sbGNsWWtZMnhoYzNOZkVCcE9URk5sYzNOcGIyNUJZM1JwZG1sMGVVZHZZV3hXWVd4MVpZQUNFQUtBQnlOQTAxWUFBQUFBQU5NUkVnb1RGQlZZVm1Gc2RXVkxaWGxYVlc1cGRFdGxlU05BMDFZQUFBQUFBSUFEZ0FiU0NoY1lHVjhRRDBoTFZXNXBkRk4wY21sdVowdGxlWUFGZ0FSUmM5SWNIUjRqV0NSamJHRnpjMlZ6V2lSamJHRnpjMjVoYldXa0h5QWhJbHBJUzFScGJXVlZibWwwV2toTFFtRnpaVlZ1YVhSV1NFdFZibWwwV0U1VFQySnFaV04wV2toTFZHbHRaVlZ1YVhUU0hCMGxKNkltSWxwSVMxRjFZVzUwYVhSNVdraExVWFZoYm5ScGRIblNIQjBwSzZJcUlsOFFGVTVNVTJWemMybHZia0ZqZEdsMmFYUjVSMjloYkY4UUZVNU1VMlZ6YzJsdmJrRmpkR2wyYVhSNVIyOWhiQklBQVlhZ1h4QVBUbE5MWlhsbFpFRnlZMmhwZG1WeTBTOHdWSEp2YjNTQUFRQUlBQkVBR2dBakFDMEFNZ0E3QUVFQVNnQnFBSlFBbXdDNEFMb0F2QUMrQU1jQXpnRFhBTjhBNkFEcUFPd0E4UUVEQVFVQkJ3RUpBUTRCRndFaUFTY0JNZ0U5QVVRQlRRRllBVjBCWUFGckFYWUJld0YrQVpZQnJnR3pBY1VCeUFITkFBQUFBQUFBQWdFQUFBQUFBQUFBTVFBQUFBQUFBQUFBQUFBQUFBQUFBYzg9IiwidXVpZCI6IjQxQTAzQkFCLTlDMjktNDhGMC1CRTZELTFFOEJDREZCMjVEOSIsIm9jY3VycmVuY2UiOnsiY291bnQiOjAsImNyZWF0aW9uRGF0ZSI6Njg0NjY0MzE2LjkyODk5ODM1LCJtb2RpZmljYXRpb25EYXRlIjo3MDIxMDc0MDMuOTU0MjE2OTYsImNvdW50TW9kaWZpY2F0aW9uRGF0ZSI6Njg0NjY0MzE2LjkyODk5ODM1fSwiYWN0aXZpdHlUeXBlIjoiWW5Cc2FYTjBNRERVQVFJREJBVWtKU1pZSkc5aWFtVmpkSE5ZSkhabGNuTnBiMjVaSkdGeVkyaHBkbVZ5VkNSMGIzQ2xCZ2NVR2lCVkpHNTFiR3pXQ0FrS0N3d05EZzhPRVJJVFh4QWZSa2xWU1ZkdmNtdHZkWFJCWTNScGRtbDBlVlI1Y0dWSmMwbHVaRzl2Y2w4UUgwWkpWVWxYYjNKcmIzVjBRV04wYVhacGRIbFVlWEJsVFdWMFlXUmhkR0ZmRUNkR1NWVkpWMjl5YTI5MWRFRmpkR2wyYVhSNVZIbHdaVkJoY25SUFprMTFiSFJwYzNCdmNuUldKR05zWVhOelh4QWhSa2xWU1ZkdmNtdHZkWFJCWTNScGRtbDBlVlI1Y0dWSlpHVnVkR2xtYVdWeVh4QW9Ua3hUWlhOemFXOXVRWFY0YVd4cFlYSjVRV04wYVhacGRIbFVlWEJsU1dSbGJuUnBabWxsY2dpQUFnaUFCQkFZRUFEVEZRc1dGeGdaV2s1VExtOWlhbVZqZEhOWFRsTXVhMlY1YzZDQUE2RFNHeHdkSGxna1kyeGhjM05sYzFva1kyeGhjM051WVcxbG9oNGZYRTVUUkdsamRHbHZibUZ5ZVZoT1UwOWlhbVZqZE5JYkhDRWpvaUlmWHhBWFJrbFZTVmR2Y210dmRYUkJZM1JwZG1sMGVWUjVjR1ZmRUJkR1NWVkpWMjl5YTI5MWRFRmpkR2wyYVhSNVZIbHdaUklBQVlhZ1h4QVBUbE5MWlhsbFpFRnlZMmhwZG1WeTBTY29WSEp2YjNTQUFRQUlBQkVBR2dBakFDMEFNZ0E0QUQ0QVN3QnRBSThBdVFEQUFPUUJEd0VRQVJJQkV3RVZBUmNCR1FFZ0FTc0JNd0UwQVRZQk53RThBVVVCVUFGVEFXQUJhUUZ1QVhFQml3R2xBYW9CdkFHXC9BY1FBQUFBQUFBQUNBUUFBQUFBQUFBQXBBQUFBQUFBQUFBQUFBQUFBQUFBQnhnPT0ifQ==","objectModificationDate":702107514.08573401,"version":1,"numbers":{"configuration_type":1,"goal_type":2},"uuid":"41A03BAB-9C29-48F0-BE6D-1E8BCDFB25D9","objectState":0} -""".data(using: .utf8)!), - .privateWorkoutWeatherLocationCoordinatesLongitude : .number(value: 11.1684857327149), - .privateWorkoutHeartRateZonesConfigurationType : .number(value: 0.0), - .privateWorkoutAverageHeartRate : .numerical(value: 1.83192725788713, unit: "count/s"), - .privateWorkoutExtendedMode : .number(value: 0.0), - .privateWorkoutMaxHeartRate : .numerical(value: 2.58333333333333, unit: "count/s"), - .privateWorkoutElapsedTimeInHeartRateZones : .data(value: .init(hex: "62706c6973743030d30102030405065131513251302340ad88000014000023407c37d7626000002340d637c27a270000080f1113151e270000000000000101000000000000000700000000000000000000000000000030")!) +""".data(using: .utf8)!, + "_HKPrivateWorkoutWeatherLocationCoordinatesLongitude" : NSNumber(11.1684857327149), + "_HKPrivateWorkoutHeartRateZonesConfigurationType" : NSNumber(0.0), + "_HKPrivateWorkoutAverageHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 1.83192725788713), + "_HKPrivateWorkoutExtendedMode" : NSNumber(0.0), + "_HKPrivateWorkoutMaxHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 2.58333333333333), + "_HKPrivateWorkoutElapsedTimeInHeartRateZones" : Data(hex: "62706c6973743030d30102030405065131513251302340ad88000014000023407c37d7626000002340d637c27a270000080f1113151e270000000000000101000000000000000700000000000000000000000000000030")! ] } diff --git a/HealthImport/Preview Content/Workout+Mock.swift b/HealthImport/Preview Content/Workout+Mock.swift index 9bfe12a..e4d84af 100644 --- a/HealthImport/Preview Content/Workout+Mock.swift +++ b/HealthImport/Preview Content/Workout+Mock.swift @@ -1,5 +1,7 @@ import Foundation import HealthKit +import HKDatabase +import HealthKitExtensions extension Workout { diff --git a/HealthImport/Preview Content/WorkoutEvent+Mock.swift b/HealthImport/Preview Content/WorkoutEvent+Mock.swift index ff633f5..7c22c7f 100644 --- a/HealthImport/Preview Content/WorkoutEvent+Mock.swift +++ b/HealthImport/Preview Content/WorkoutEvent+Mock.swift @@ -8,15 +8,36 @@ extension HKWorkoutEvent { .init(type: .init(rawValue: 7)!, dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307), duration: 1114.56374406815), - metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event1Metadata)!)), + metadata: [ + "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000), + "_HKPrivateWorkoutSegmentEventSubtype": NSNumber(1), + "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000), + "_HKPrivateMetadataSplitMeasuringSystem": NSNumber(1), + "_HKPrivateMetadataIsPartialSplit": NSNumber(0), + "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1114.56) + ]), .init(type: .init(rawValue: 7)!, dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307), duration: 1972.17168283463), - metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)), + metadata: [ + "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), + "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17), + "_HKPrivateMetadataIsPartialSplit": 0, + "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), + "_HKPrivateWorkoutSegmentEventSubtype": 1, + "_HKPrivateMetadataSplitMeasuringSystem": 2 + ]), .init(type: .init(rawValue: 1)!, dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113), duration: 0.0), - metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)), + metadata: [ + "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), + "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17), + "_HKPrivateMetadataIsPartialSplit": 0, + "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), + "_HKPrivateWorkoutSegmentEventSubtype": 1, + "_HKPrivateMetadataSplitMeasuringSystem": 2 + ]), .init(type: .init(rawValue: 2)!, dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132), duration: 0.0), @@ -24,7 +45,3 @@ extension HKWorkoutEvent { ] } } - -private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001" - -private let mock1Event2Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c094c3789416025994012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c09882da1cdafd09e401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c094c3789416025994012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2002" diff --git a/HealthImport/SampleListView.swift b/HealthImport/SampleListView.swift index 8ff2332..4293de8 100644 --- a/HealthImport/SampleListView.swift +++ b/HealthImport/SampleListView.swift @@ -1,10 +1,11 @@ import SwiftUI +import HealthKit struct SampleListView: View { - let type: Sample.DataType + let type: HKSampleType - let samples: [Sample] + let samples: [HKSample] var body: some View { List { @@ -21,9 +22,9 @@ struct SampleListView: View { } */ -extension Sample: Identifiable { +extension HKSample: Identifiable { - var id: Date { - startDate + public var id: UUID { + uuid } } diff --git a/HealthImport/Support/HKWorkoutActivityType+Extensions.swift b/HealthImport/Support/HKWorkoutActivityType+Extensions.swift deleted file mode 100644 index 37d5e62..0000000 --- a/HealthImport/Support/HKWorkoutActivityType+Extensions.swift +++ /dev/null @@ -1,187 +0,0 @@ -import Foundation -import HealthKit - -extension HKWorkoutActivityType: CustomStringConvertible { - - public var description: String { - switch self { - case .climbing: - return "Climbing" - case .cycling: - return "Cycling" - case .hiking: - return "Hiking" - case .hockey: - return "Hockey" - case .other: - return "Other" - case .rowing: - return "Rowing" - case .running: - return "Running" - case .swimming: - return "Swimming" - case .yoga: - return "Yoga" - case .walking: - return "Walking" - case .americanFootball: - return "American Football" - case .archery: - return "Archery" - case .australianFootball: - return "Australian Football" - case .badminton: - return "Badminton" - case .baseball: - return "Baseball" - case .basketball: - return "Basketball" - case .bowling: - return "Bowling" - case .boxing: - return "Boxing" - case .cricket: - return "Cricket" - case .crossTraining: - return "Cross Training" - case .curling: - return "Curling" - case .dance: - return "Dance" - case .danceInspiredTraining: - return "Dance Inspired Training" - case .elliptical: - return "Elliptical" - case .equestrianSports: - return "Equestrian Sports" - case .fencing: - return "Fencing" - case .fishing: - return "Fishing" - case .functionalStrengthTraining: - return "Functional Strength Training" - case .golf: - return "Golf" - case .gymnastics: - return "Gymnastics" - case .handball: - return "Handball" - case .hunting: - return "Hunting" - case .lacrosse: - return "Lacrosse" - case .martialArts: - return "Martial Arts" - case .mindAndBody: - return "Mind And Body" - case .mixedMetabolicCardioTraining: - return "Mixed Metabolic Cardio Training" - case .paddleSports: - return "Paddle Sports" - case .play: - return "Play" - case .preparationAndRecovery: - return "Preparation and Recovery" - case .racquetball: - return "Racquetball" - case .rugby: - return "Rugby" - case .sailing: - return "Sailing" - case .skatingSports: - return "Skating Sports" - case .snowSports: - return "Snow Sports" - case .soccer: - return "Soccer" - case .softball: - return "Softball" - case .squash: - return "Squash" - case .stairClimbing: - return "Stair Climbing" - case .surfingSports: - return "Surfing Sports" - case .tableTennis: - return "Table Tennis" - case .tennis: - return "Tennis" - case .trackAndField: - return "Track And Field" - case .traditionalStrengthTraining: - return "Traditional Strength Training" - case .volleyball: - return "Volleyball" - case .waterFitness: - return "Water Fitness" - case .waterPolo: - return "Water Polo" - case .waterSports: - return "Water Sports" - case .wrestling: - return "Wrestling" - case .barre: - return "Barre" - case .coreTraining: - return "Core Training" - case .crossCountrySkiing: - return "Cross Country Skiing" - case .downhillSkiing: - return "Downholl Skiing" - case .flexibility: - return "Flexibility" - case .highIntensityIntervalTraining: - return "High Intensity Interval Training" - case .jumpRope: - return "Jump Rope" - case .kickboxing: - return "Kickboxing" - case .pilates: - return "Pilates" - case .snowboarding: - return "Snowboarding" - case .stairs: - return "Stairs" - case .stepTraining: - return "Step Training" - case .wheelchairWalkPace: - return "Wheelchair Walk Pace" - case .wheelchairRunPace: - return "Wheelchair Run Pace" - case .taiChi: - return "Tai Chi" - case .mixedCardio: - return "Mixed Cardio" - case .handCycling: - return "Hand Cycling" - case .discSports: - return "Disc Sports" - case .fitnessGaming: - return "Fitness Gaming" - case .cardioDance: - return "Cardio Dance" - case .socialDance: - return "Social Dance" - case .pickleball: - return "Pickleball" - case .cooldown: - return "Cooldown" - case .swimBikeRun: - return "Triathlon" - case .transition: - return "Transition" - case .underwaterDiving: - return "Underwater Diving" - @unknown default: - return "\(rawValue)" - } - } -} - -extension HKWorkoutActivityType: Comparable { - - public static func < (lhs: HKWorkoutActivityType, rhs: HKWorkoutActivityType) -> Bool { - lhs.rawValue < rhs.rawValue - } -} diff --git a/HealthImport/Support/HKWorkoutEventType+Extensions.swift b/HealthImport/Support/HKWorkoutEventType+Extensions.swift deleted file mode 100644 index 0e50084..0000000 --- a/HealthImport/Support/HKWorkoutEventType+Extensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import HealthKit - -extension HKWorkoutEventType: CustomStringConvertible { - - public var description: String { - switch self { - case .pause: return "Pause" - case .resume: return "Resume" - case .lap: return "Lap" - case .marker: return "Marker" - case .motionPaused: return "Motion Paused" - case .motionResumed: return "Motion Resumed" - case .segment: return "Segment" - case .pauseOrResumeRequest: return "Pause or Resume Request" - @unknown default: - return "Unknown" - } - } -} diff --git a/HealthImport/Support/HKWorkoutSessionLocationType+Extensions.swift b/HealthImport/Support/HKWorkoutSessionLocationType+Extensions.swift deleted file mode 100644 index 29db586..0000000 --- a/HealthImport/Support/HKWorkoutSessionLocationType+Extensions.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import HealthKit - -extension HKWorkoutSessionLocationType: CustomStringConvertible { - - public var description: String { - switch self { - case .unknown: - return "Unknown" - case .indoor: - return "Indoor" - case .outdoor: - return "Outdoor" - @unknown default: - return "Unknown default" - } - } -} diff --git a/HealthImport/Support/HKWorkoutSwimmingLocationType+Extensions.swift b/HealthImport/Support/HKWorkoutSwimmingLocationType+Extensions.swift deleted file mode 100644 index 319de46..0000000 --- a/HealthImport/Support/HKWorkoutSwimmingLocationType+Extensions.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import HealthKit - -extension HKWorkoutSwimmingLocationType: CustomStringConvertible { - - public var description: String { - switch self { - case .unknown: - return "Unknown" - case .pool: - return "Pool" - case .openWater: - return "Open Water" - @unknown default: - return "Unknown default" - } - } -} diff --git a/HealthImport/Support/Workout+Extensions.swift b/HealthImport/Support/Workout+Extensions.swift new file mode 100644 index 0000000..c1953fc --- /dev/null +++ b/HealthImport/Support/Workout+Extensions.swift @@ -0,0 +1,36 @@ +import Foundation +import HKDatabase + +private let df: DateFormatter = { + let df = DateFormatter() + df.timeZone = .current + df.dateStyle = .short + df.timeStyle = .short + return df +}() + +extension Workout { + + var typeString: String { + activities.first?.workoutConfiguration.activityType.description ?? "Unknown activity" + } + + var dateString: String { + guard let firstAvailableDate else { + return "No date" + } + return df.string(from: firstAvailableDate) + } + + var firstActivityDate: Date? { + activities.map { $0.startDate }.min() + } + + var firstEventDate: Date? { + events.map { $0.dateInterval.start }.min() + } + + var firstAvailableDate: Date? { + [firstEventDate, firstActivityDate].compactMap { $0 }.min() + } +} diff --git a/HealthImport/WorkoutDetailView.swift b/HealthImport/WorkoutDetailView.swift index adb88be..1d9ef03 100644 --- a/HealthImport/WorkoutDetailView.swift +++ b/HealthImport/WorkoutDetailView.swift @@ -1,22 +1,22 @@ import SwiftUI import Collections import HealthKit +import HKDatabase struct WorkoutDetailView: View { - @EnvironmentObject - var database: HealthDatabase - let workout: Workout - + + var metadata: [(key: String, value: Any)] { + workout.metadata.sorted { $0.key } + } + var body: some View { List { Section("Info") { DetailRow("ID", value: workout.id) DetailRow("Total Distance", kilometer: workout.totalDistance) DetailRow("Goal", value: workout.goal) - DetailRow("Condenser Version", value: workout.condenserVersion) - DetailRow("Condenser Date", date: workout.condenserDate) } if !workout.activities.isEmpty { Section("Activities") { @@ -40,8 +40,8 @@ struct WorkoutDetailView: View { } if !workout.metadata.isEmpty { Section("Metadata") { - ForEach(workout.metadata.elements, id:\.key) { (key, value) in - DetailRow(key.description, value: value) + ForEach(metadata, id:\.key) { (key, value) in + DetailRow("\(key)", value: "\(value)") } } } @@ -49,7 +49,6 @@ struct WorkoutDetailView: View { .navigationTitle(workout.typeString) .navigationDestination(for: HKWorkoutActivity.self) { activity in ActivityDetailView(activity: activity) - .environmentObject(database) } .navigationDestination(for: HKWorkoutEvent.self) { event in EventDetailView(event: event) @@ -58,9 +57,9 @@ struct WorkoutDetailView: View { } #Preview { - NavigationStack { + HealthDatabase.shared = .mock() + return NavigationStack { WorkoutDetailView(workout: .mock1) - .environmentObject(HealthDatabase.mock()) } }