Compare commits

...

14 Commits

Author SHA1 Message Date
Christoph Hagen
9b07f498dd Update dependencies, fix warnings 2025-01-31 14:19:35 +01:00
Christoph Hagen
3bfb256026 Create .gitignore 2025-01-31 11:59:07 +01:00
Christoph Hagen
1a0550523f Update logo, add image to readme 2024-04-15 23:40:32 +02:00
Christoph Hagen
29d38f3ad6 Add short readme 2024-04-15 16:23:53 +02:00
Christoph Hagen
03469446e1 Add view of most samples 2024-04-15 15:55:10 +02:00
Christoph Hagen
778e83682a Add logo 2024-04-13 11:35:32 +02:00
Christoph Hagen
db0e7980de Update dependency 2024-03-22 10:16:24 +01:00
Christoph Hagen
75a18d0882 Update dependency 2024-03-22 10:09:10 +01:00
Christoph Hagen
98dffd56e4 Organize files 2024-03-20 14:51:46 +01:00
Christoph Hagen
ee1993e757 Allow deletion of workouts 2024-03-20 14:48:01 +01:00
Christoph Hagen
7e66f81aa6 Remove unused views 2024-03-20 14:47:42 +01:00
Christoph Hagen
e5670afc22 Show private metadata and statistics for activity 2024-03-20 10:19:08 +01:00
Christoph Hagen
26e06ffc06 Cleanup 2024-03-19 19:26:43 +01:00
Christoph Hagen
5d1b3d88f7 Show errors on import 2024-03-19 17:57:43 +01:00
65 changed files with 1327 additions and 645 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

BIN
HealthImport.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

BIN
HealthImport.key Executable file

Binary file not shown.

View File

@@ -11,8 +11,6 @@
8850025D2B5C273C00E7D4DB /* WorkoutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850025C2B5C273C00E7D4DB /* WorkoutTab.swift */; }; 8850025D2B5C273C00E7D4DB /* WorkoutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850025C2B5C273C00E7D4DB /* WorkoutTab.swift */; };
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; }; 8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; };
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 885002612B5C273E00E7D4DB /* Preview 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 */; };
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; };
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; }; 885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; };
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.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 */; }; 8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */; };
@@ -25,28 +23,44 @@
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A92B5D296700E7D4DB /* OrderedCollections */; }; 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A92B5D296700E7D4DB /* OrderedCollections */; };
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */; }; 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 */; }; E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC742B626B19005B83D3 /* Metadata+Mock.swift */; };
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; };
E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E20881D22B76912000D41D95 /* HealthKitExtensions */; }; 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 */; }; E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; };
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; }; E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; };
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6812B5E762D003A8873 /* LocationSampleDetailView.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 */; }; E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6832B5E76A4003A8873 /* Location+Mock.swift */; };
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; };
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.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 */; }; 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 */; }; 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 */; }; E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; };
E29E17BF2D4D04A200E0EE54 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = E29E17BE2D4D04A200E0EE54 /* SQLite */; };
E29E17C22D4D04B900E0EE54 /* HealthDB in Frameworks */ = {isa = PBXBuildFile; productRef = E29E17C12D4D04B900E0EE54 /* HealthDB */; };
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; }; E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; };
E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */; }; E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */; };
E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */; }; E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */; };
E2D82B2A2BCD25B60075EAF0 /* QuantitySampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B292BCD25B60075EAF0 /* QuantitySampleList.swift */; };
E2D82B2C2BCD28720075EAF0 /* QuantitySampleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B2B2BCD28720075EAF0 /* QuantitySampleRow.swift */; };
E2D82B2E2BCD319D0075EAF0 /* BodyMeasurementsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B2D2BCD319D0075EAF0 /* BodyMeasurementsList.swift */; };
E2D82B302BCD32F20075EAF0 /* CycleTrackingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B2F2BCD32F20075EAF0 /* CycleTrackingList.swift */; };
E2D82B322BCD34B80075EAF0 /* CategoryEnumSampleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B312BCD34B80075EAF0 /* CategoryEnumSampleRow.swift */; };
E2D82B342BCD34EB0075EAF0 /* CategoryEmptySampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B332BCD34EB0075EAF0 /* CategoryEmptySampleList.swift */; };
E2D82B362BCD35DD0075EAF0 /* CategoryEnumSampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B352BCD35DD0075EAF0 /* CategoryEnumSampleList.swift */; };
E2D82B3B2BCD38E30075EAF0 /* CategoryEmptySampleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B3A2BCD38E30075EAF0 /* CategoryEmptySampleRow.swift */; };
E2D82B3D2BCD3C3F0075EAF0 /* HearingSamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B3C2BCD3C3F0075EAF0 /* HearingSamplesList.swift */; };
E2D82B3F2BCD47E80075EAF0 /* HeartSamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B3E2BCD47E80075EAF0 /* HeartSamplesList.swift */; };
E2D82B432BCD53D10075EAF0 /* GenericSampleList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B422BCD53D10075EAF0 /* GenericSampleList.swift */; };
E2D82B452BCD582D0075EAF0 /* MentalHealthList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B442BCD582D0075EAF0 /* MentalHealthList.swift */; };
E2D82B472BCD59380075EAF0 /* MobilitySamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B462BCD59380075EAF0 /* MobilitySamplesList.swift */; };
E2D82B492BCD5BA00075EAF0 /* NutritionSamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B482BCD5BA00075EAF0 /* NutritionSamplesList.swift */; };
E2D82B4B2BCD5E520075EAF0 /* RespiratorySamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B4A2BCD5E520075EAF0 /* RespiratorySamplesList.swift */; };
E2D82B4D2BCD5F780075EAF0 /* SymptomsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B4C2BCD5F780075EAF0 /* SymptomsList.swift */; };
E2D82B4F2BCD61590075EAF0 /* VitalsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B4E2BCD61590075EAF0 /* VitalsList.swift */; };
E2D82B512BCD626D0075EAF0 /* OtherSamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D82B502BCD626D0075EAF0 /* OtherSamplesList.swift */; };
E2E552892BA2194400BF5E9B /* DatabasesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552882BA2194400BF5E9B /* DatabasesTab.swift */; }; E2E552892BA2194400BF5E9B /* DatabasesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552882BA2194400BF5E9B /* DatabasesTab.swift */; };
E2E5528C2BA21C0700BF5E9B /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528B2BA21C0700BF5E9B /* HealthDatabase.swift */; }; E2E5528C2BA21C0700BF5E9B /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528B2BA21C0700BF5E9B /* Database.swift */; };
E2E5528E2BA21C5900BF5E9B /* FileManager+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528D2BA21C5900BF5E9B /* FileManager+Directory.swift */; }; E2E5528E2BA21C5900BF5E9B /* FileManager+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528D2BA21C5900BF5E9B /* FileManager+Directory.swift */; };
E2E552902BA236A000BF5E9B /* DatabaseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */; }; E2E552902BA236A000BF5E9B /* DatabaseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */; };
E2E552922BA236D000BF5E9B /* DatabaseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552912BA236D000BF5E9B /* DatabaseFile.swift */; }; E2E552922BA236D000BF5E9B /* DatabaseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552912BA236D000BF5E9B /* DatabaseFile.swift */; };
E2E5529B2BA3935600BF5E9B /* HKWorkout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5529A2BA3935600BF5E9B /* HKWorkout+Extensions.swift */; }; E2E5529B2BA3935600BF5E9B /* HKWorkout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5529A2BA3935600BF5E9B /* HKWorkout+Extensions.swift */; };
E2E5529E2BA47BA600BF5E9B /* HealthDB in Frameworks */ = {isa = PBXBuildFile; productRef = E2E5529D2BA47BA600BF5E9B /* HealthDB */; };
E2E552A12BA4B14600BF5E9B /* HeartRateSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */; }; E2E552A12BA4B14600BF5E9B /* HeartRateSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */; };
E2E552A32BA4B58F00BF5E9B /* HeartRateGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */; }; E2E552A32BA4B58F00BF5E9B /* HeartRateGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */; };
E2E552A72BA7531C00BF5E9B /* Event+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A62BA7531C00BF5E9B /* Event+Identifiable.swift */; }; E2E552A72BA7531C00BF5E9B /* Event+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552A62BA7531C00BF5E9B /* Event+Identifiable.swift */; };
@@ -59,6 +73,7 @@
E2E552B72BA9A69400BF5E9B /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */; }; E2E552B72BA9A69400BF5E9B /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */; };
E2E552B92BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */; }; E2E552B92BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */; };
E2E552BB2BA9CAAE00BF5E9B /* SamplesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552BA2BA9CAAE00BF5E9B /* SamplesTab.swift */; }; E2E552BB2BA9CAAE00BF5E9B /* SamplesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552BA2BA9CAAE00BF5E9B /* SamplesTab.swift */; };
E2E552C02BAB38DC00BF5E9B /* ActivitySamplesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552BF2BAB38DC00BF5E9B /* ActivitySamplesList.swift */; };
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; }; E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; }; E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -69,7 +84,6 @@
8850025C2B5C273C00E7D4DB /* WorkoutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutTab.swift; sourceTree = "<group>"; }; 8850025C2B5C273C00E7D4DB /* WorkoutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutTab.swift; sourceTree = "<group>"; };
8850025E2B5C273E00E7D4DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 8850025E2B5C273E00E7D4DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
885002612B5C273E00E7D4DB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthdb_secure.sqlite; sourceTree = "<group>"; };
885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; }; 885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = "<group>"; }; 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = "<group>"; };
8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; }; 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
@@ -79,25 +93,42 @@
8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; }; 8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+Mock.swift"; sourceTree = "<group>"; }; E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+Mock.swift"; sourceTree = "<group>"; };
E201EC742B626B19005B83D3 /* Metadata+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Metadata+Mock.swift"; sourceTree = "<group>"; }; E201EC742B626B19005B83D3 /* Metadata+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Metadata+Mock.swift"; sourceTree = "<group>"; };
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = "<group>"; };
E20881D42B76944A00D41D95 /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = "<group>"; };
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; }; E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; };
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = "<group>"; }; E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = "<group>"; };
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleDetailView.swift; sourceTree = "<group>"; }; E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleDetailView.swift; sourceTree = "<group>"; };
E27BC6832B5E76A4003A8873 /* Location+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+Mock.swift"; sourceTree = "<group>"; }; E27BC6832B5E76A4003A8873 /* Location+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+Mock.swift"; sourceTree = "<group>"; };
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesView.swift; sourceTree = "<group>"; };
E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HealthDatabase+Mock.swift"; sourceTree = "<group>"; }; E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HealthDatabase+Mock.swift"; sourceTree = "<group>"; };
E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = "<group>"; }; E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = "<group>"; };
E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = "<group>"; }; E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = "<group>"; };
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; }; E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = "<group>"; }; E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = "<group>"; };
E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHealthStoreView.swift; sourceTree = "<group>"; }; E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHealthStoreView.swift; sourceTree = "<group>"; };
E2D82B292BCD25B60075EAF0 /* QuantitySampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySampleList.swift; sourceTree = "<group>"; };
E2D82B2B2BCD28720075EAF0 /* QuantitySampleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySampleRow.swift; sourceTree = "<group>"; };
E2D82B2D2BCD319D0075EAF0 /* BodyMeasurementsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyMeasurementsList.swift; sourceTree = "<group>"; };
E2D82B2F2BCD32F20075EAF0 /* CycleTrackingList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleTrackingList.swift; sourceTree = "<group>"; };
E2D82B312BCD34B80075EAF0 /* CategoryEnumSampleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEnumSampleRow.swift; sourceTree = "<group>"; };
E2D82B332BCD34EB0075EAF0 /* CategoryEmptySampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEmptySampleList.swift; sourceTree = "<group>"; };
E2D82B352BCD35DD0075EAF0 /* CategoryEnumSampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEnumSampleList.swift; sourceTree = "<group>"; };
E2D82B372BCD36A90075EAF0 /* healthdb_secure.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthdb_secure.sqlite; sourceTree = "<group>"; };
E2D82B3A2BCD38E30075EAF0 /* CategoryEmptySampleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEmptySampleRow.swift; sourceTree = "<group>"; };
E2D82B3C2BCD3C3F0075EAF0 /* HearingSamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HearingSamplesList.swift; sourceTree = "<group>"; };
E2D82B3E2BCD47E80075EAF0 /* HeartSamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartSamplesList.swift; sourceTree = "<group>"; };
E2D82B422BCD53D10075EAF0 /* GenericSampleList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericSampleList.swift; sourceTree = "<group>"; };
E2D82B442BCD582D0075EAF0 /* MentalHealthList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentalHealthList.swift; sourceTree = "<group>"; };
E2D82B462BCD59380075EAF0 /* MobilitySamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobilitySamplesList.swift; sourceTree = "<group>"; };
E2D82B482BCD5BA00075EAF0 /* NutritionSamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NutritionSamplesList.swift; sourceTree = "<group>"; };
E2D82B4A2BCD5E520075EAF0 /* RespiratorySamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RespiratorySamplesList.swift; sourceTree = "<group>"; };
E2D82B4C2BCD5F780075EAF0 /* SymptomsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymptomsList.swift; sourceTree = "<group>"; };
E2D82B4E2BCD61590075EAF0 /* VitalsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalsList.swift; sourceTree = "<group>"; };
E2D82B502BCD626D0075EAF0 /* OtherSamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherSamplesList.swift; sourceTree = "<group>"; };
E2D82B522BCD6CC10075EAF0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
E2E552882BA2194400BF5E9B /* DatabasesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabasesTab.swift; sourceTree = "<group>"; }; E2E552882BA2194400BF5E9B /* DatabasesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabasesTab.swift; sourceTree = "<group>"; };
E2E5528B2BA21C0700BF5E9B /* HealthDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthDatabase.swift; sourceTree = "<group>"; }; E2E5528B2BA21C0700BF5E9B /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
E2E5528D2BA21C5900BF5E9B /* FileManager+Directory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Directory.swift"; sourceTree = "<group>"; }; E2E5528D2BA21C5900BF5E9B /* FileManager+Directory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Directory.swift"; sourceTree = "<group>"; };
E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseList.swift; sourceTree = "<group>"; }; E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseList.swift; sourceTree = "<group>"; };
E2E552912BA236D000BF5E9B /* DatabaseFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFile.swift; sourceTree = "<group>"; }; E2E552912BA236D000BF5E9B /* DatabaseFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFile.swift; sourceTree = "<group>"; };
E2E552932BA23B8F00BF5E9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; E2E552932BA23B8F00BF5E9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E2E5529A2BA3935600BF5E9B /* HKWorkout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkout+Extensions.swift"; sourceTree = "<group>"; }; E2E5529A2BA3935600BF5E9B /* HKWorkout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkout+Extensions.swift"; sourceTree = "<group>"; };
E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateSample.swift; sourceTree = "<group>"; }; E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateSample.swift; sourceTree = "<group>"; };
E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateGraph.swift; sourceTree = "<group>"; }; E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateGraph.swift; sourceTree = "<group>"; };
@@ -111,6 +142,7 @@
E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; }; E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivityType+Icon.swift"; sourceTree = "<group>"; }; E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivityType+Icon.swift"; sourceTree = "<group>"; };
E2E552BA2BA9CAAE00BF5E9B /* SamplesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTab.swift; sourceTree = "<group>"; }; E2E552BA2BA9CAAE00BF5E9B /* SamplesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTab.swift; sourceTree = "<group>"; };
E2E552BF2BAB38DC00BF5E9B /* ActivitySamplesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesList.swift; sourceTree = "<group>"; };
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; }; E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = "<group>"; }; E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -122,10 +154,10 @@
files = ( files = (
885002A62B5D296700E7D4DB /* Collections in Frameworks */, 885002A62B5D296700E7D4DB /* Collections in Frameworks */,
E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */, E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */,
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */, 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */,
E2E5529E2BA47BA600BF5E9B /* HealthDB in Frameworks */,
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */, 885002A82B5D296700E7D4DB /* DequeModule in Frameworks */,
E29E17C22D4D04B900E0EE54 /* HealthDB in Frameworks */,
E29E17BF2D4D04A200E0EE54 /* SQLite in Frameworks */,
E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */, E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */,
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */, E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */,
); );
@@ -137,6 +169,7 @@
8850024E2B5C273C00E7D4DB = { 8850024E2B5C273C00E7D4DB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2D82B522BCD6CC10075EAF0 /* README.md */,
885002592B5C273C00E7D4DB /* HealthImport */, 885002592B5C273C00E7D4DB /* HealthImport */,
885002582B5C273C00E7D4DB /* Products */, 885002582B5C273C00E7D4DB /* Products */,
E2E5529C2BA47BA600BF5E9B /* Frameworks */, E2E5529C2BA47BA600BF5E9B /* Frameworks */,
@@ -154,26 +187,17 @@
885002592B5C273C00E7D4DB /* HealthImport */ = { 885002592B5C273C00E7D4DB /* HealthImport */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
885002832B5C37C600E7D4DB /* Extensions */,
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */,
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
E2E552932BA23B8F00BF5E9B /* Info.plist */, E2E552932BA23B8F00BF5E9B /* Info.plist */,
E2E5528A2BA21BFB00BF5E9B /* Model */, E2E5528A2BA21BFB00BF5E9B /* Model */,
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */,
8850026A2B5C276B00E7D4DB /* Resources */,
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
E2E552872BA2193B00BF5E9B /* Tabs */,
E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */,
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
E2E5529F2BA4B13100BF5E9B /* UI Elements */,
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */,
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */,
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */,
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */,
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
885002942B5D147100E7D4DB /* DetailRow.swift */,
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
885002602B5C273E00E7D4DB /* Preview Content */, 885002602B5C273E00E7D4DB /* Preview Content */,
E20881D42B76944A00D41D95 /* Test.swift */, E2E552BE2BAB38AC00BF5E9B /* Samples */,
885002832B5C37C600E7D4DB /* Support */, E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */,
E2E552872BA2193B00BF5E9B /* Tabs */,
E2E552BC2BAAE9A900BF5E9B /* Workouts */,
); );
path = HealthImport; path = HealthImport;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -181,6 +205,7 @@
885002602B5C273E00E7D4DB /* Preview Content */ = { 885002602B5C273E00E7D4DB /* Preview Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2D82B372BCD36A90075EAF0 /* healthdb_secure.sqlite */,
885002612B5C273E00E7D4DB /* Preview Assets.xcassets */, 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */,
E27BC6832B5E76A4003A8873 /* Location+Mock.swift */, E27BC6832B5E76A4003A8873 /* Location+Mock.swift */,
E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */, E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */,
@@ -192,15 +217,7 @@
path = "Preview Content"; path = "Preview Content";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
8850026A2B5C276B00E7D4DB /* Resources */ = { 885002832B5C37C600E7D4DB /* Extensions */ = {
isa = PBXGroup;
children = (
8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */,
);
path = Resources;
sourceTree = "<group>";
};
885002832B5C37C600E7D4DB /* Support */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */, E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */,
@@ -218,7 +235,26 @@
E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */, E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */,
E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */, E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */,
); );
path = Support; path = Extensions;
sourceTree = "<group>";
};
E2D82B392BCD37FA0075EAF0 /* Lists */ = {
isa = PBXGroup;
children = (
E2E552BF2BAB38DC00BF5E9B /* ActivitySamplesList.swift */,
E2D82B2D2BCD319D0075EAF0 /* BodyMeasurementsList.swift */,
E2D82B2F2BCD32F20075EAF0 /* CycleTrackingList.swift */,
E2D82B3C2BCD3C3F0075EAF0 /* HearingSamplesList.swift */,
E2D82B3E2BCD47E80075EAF0 /* HeartSamplesList.swift */,
E2D82B442BCD582D0075EAF0 /* MentalHealthList.swift */,
E2D82B462BCD59380075EAF0 /* MobilitySamplesList.swift */,
E2D82B482BCD5BA00075EAF0 /* NutritionSamplesList.swift */,
E2D82B4A2BCD5E520075EAF0 /* RespiratorySamplesList.swift */,
E2D82B4C2BCD5F780075EAF0 /* SymptomsList.swift */,
E2D82B4E2BCD61590075EAF0 /* VitalsList.swift */,
E2D82B502BCD626D0075EAF0 /* OtherSamplesList.swift */,
);
path = Lists;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E2E552872BA2193B00BF5E9B /* Tabs */ = { E2E552872BA2193B00BF5E9B /* Tabs */ = {
@@ -234,7 +270,7 @@
E2E5528A2BA21BFB00BF5E9B /* Model */ = { E2E5528A2BA21BFB00BF5E9B /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2E5528B2BA21C0700BF5E9B /* HealthDatabase.swift */, E2E5528B2BA21C0700BF5E9B /* Database.swift */,
E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */, E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */,
E2E552912BA236D000BF5E9B /* DatabaseFile.swift */, E2E552912BA236D000BF5E9B /* DatabaseFile.swift */,
); );
@@ -248,17 +284,38 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E2E5529F2BA4B13100BF5E9B /* UI Elements */ = { E2E552BC2BAAE9A900BF5E9B /* Workouts */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */, 885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
885002942B5D147100E7D4DB /* DetailRow.swift */,
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */, E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */,
E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */,
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */,
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */,
E2E552AC2BA98B9B00BF5E9B /* RouteView.swift */, E2E552AC2BA98B9B00BF5E9B /* RouteView.swift */,
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
E2E552B42BA9A5D200BF5E9B /* WorkoutListRow.swift */,
E2E552AE2BA98BCF00BF5E9B /* WorkoutMapView.swift */, E2E552AE2BA98BCF00BF5E9B /* WorkoutMapView.swift */,
E2E552B22BA9A1D500BF5E9B /* WorkoutTypeSelection.swift */, E2E552B22BA9A1D500BF5E9B /* WorkoutTypeSelection.swift */,
E2E552B42BA9A5D200BF5E9B /* WorkoutListRow.swift */,
); );
path = "UI Elements"; path = Workouts;
sourceTree = "<group>";
};
E2E552BE2BAB38AC00BF5E9B /* Samples */ = {
isa = PBXGroup;
children = (
E2D82B392BCD37FA0075EAF0 /* Lists */,
E2D82B332BCD34EB0075EAF0 /* CategoryEmptySampleList.swift */,
E2D82B352BCD35DD0075EAF0 /* CategoryEnumSampleList.swift */,
E2D82B312BCD34B80075EAF0 /* CategoryEnumSampleRow.swift */,
E2D82B3A2BCD38E30075EAF0 /* CategoryEmptySampleRow.swift */,
E2D82B292BCD25B60075EAF0 /* QuantitySampleList.swift */,
E2D82B422BCD53D10075EAF0 /* GenericSampleList.swift */,
E2D82B2B2BCD28720075EAF0 /* QuantitySampleRow.swift */,
);
path = Samples;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXGroup section */ /* End PBXGroup section */
@@ -278,14 +335,14 @@
); );
name = HealthImport; name = HealthImport;
packageProductDependencies = ( packageProductDependencies = (
885002762B5C2FC400E7D4DB /* SQLite */,
885002A52B5D296700E7D4DB /* Collections */, 885002A52B5D296700E7D4DB /* Collections */,
885002A72B5D296700E7D4DB /* DequeModule */, 885002A72B5D296700E7D4DB /* DequeModule */,
885002A92B5D296700E7D4DB /* OrderedCollections */, 885002A92B5D296700E7D4DB /* OrderedCollections */,
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */, E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */,
E20881D22B76912000D41D95 /* HealthKitExtensions */, E20881D22B76912000D41D95 /* HealthKitExtensions */,
E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */, E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */,
E2E5529D2BA47BA600BF5E9B /* HealthDB */, E29E17BE2D4D04A200E0EE54 /* SQLite */,
E29E17C12D4D04B900E0EE54 /* HealthDB */,
); );
productName = HealthImport; productName = HealthImport;
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */; productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
@@ -316,12 +373,12 @@
); );
mainGroup = 8850024E2B5C273C00E7D4DB; mainGroup = 8850024E2B5C273C00E7D4DB;
packageReferences = ( packageReferences = (
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */,
885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */, 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */,
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */, E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */, E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */,
E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
E2E552972BA3748500BF5E9B /* XCRemoteSwiftPackageReference "HealthDB" */, E29E17BD2D4D04A200E0EE54 /* XCRemoteSwiftPackageReference "SQLite" */,
E29E17C02D4D04B900E0EE54 /* XCRemoteSwiftPackageReference "HealthDB" */,
); );
productRefGroup = 885002582B5C273C00E7D4DB /* Products */; productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -337,7 +394,6 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */,
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */, 885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */,
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */, 8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */,
); );
@@ -350,46 +406,62 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */,
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */, E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */,
E2E552922BA236D000BF5E9B /* DatabaseFile.swift in Sources */, E2E552922BA236D000BF5E9B /* DatabaseFile.swift in Sources */,
E2D82B472BCD59380075EAF0 /* MobilitySamplesList.swift in Sources */,
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */, E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
8850025D2B5C273C00E7D4DB /* WorkoutTab.swift in Sources */, 8850025D2B5C273C00E7D4DB /* WorkoutTab.swift in Sources */,
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */, 8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */,
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */, 885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */,
E2D82B3F2BCD47E80075EAF0 /* HeartSamplesList.swift in Sources */,
E2D82B432BCD53D10075EAF0 /* GenericSampleList.swift in Sources */,
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */, E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */,
E2D82B2C2BCD28720075EAF0 /* QuantitySampleRow.swift in Sources */,
E2D82B362BCD35DD0075EAF0 /* CategoryEnumSampleList.swift in Sources */,
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */, E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */,
E2E552B92BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift in Sources */, E2E552B92BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift in Sources */,
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */, 8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
E2E552B72BA9A69400BF5E9B /* Color+Extensions.swift in Sources */, E2E552B72BA9A69400BF5E9B /* Color+Extensions.swift in Sources */,
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */, E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */,
E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */, E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */,
E2D82B302BCD32F20075EAF0 /* CycleTrackingList.swift in Sources */,
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */, 885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
E2D82B4D2BCD5F780075EAF0 /* SymptomsList.swift in Sources */,
E2E552A32BA4B58F00BF5E9B /* HeartRateGraph.swift in Sources */, E2E552A32BA4B58F00BF5E9B /* HeartRateGraph.swift in Sources */,
E2E552AF2BA98BCF00BF5E9B /* WorkoutMapView.swift in Sources */, E2E552AF2BA98BCF00BF5E9B /* WorkoutMapView.swift in Sources */,
E2D82B342BCD34EB0075EAF0 /* CategoryEmptySampleList.swift in Sources */,
E2E552BB2BA9CAAE00BF5E9B /* SamplesTab.swift in Sources */, E2E552BB2BA9CAAE00BF5E9B /* SamplesTab.swift in Sources */,
E2D82B3B2BCD38E30075EAF0 /* CategoryEmptySampleRow.swift in Sources */,
E2E552C02BAB38DC00BF5E9B /* ActivitySamplesList.swift in Sources */,
E2D82B322BCD34B80075EAF0 /* CategoryEnumSampleRow.swift in Sources */,
E2D82B492BCD5BA00075EAF0 /* NutritionSamplesList.swift in Sources */,
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */, E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
E2E5529B2BA3935600BF5E9B /* HKWorkout+Extensions.swift in Sources */, E2E5529B2BA3935600BF5E9B /* HKWorkout+Extensions.swift in Sources */,
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */, E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */, 8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
E2D82B2A2BCD25B60075EAF0 /* QuantitySampleList.swift in Sources */,
E2E552A72BA7531C00BF5E9B /* Event+Identifiable.swift in Sources */, E2E552A72BA7531C00BF5E9B /* Event+Identifiable.swift in Sources */,
E2D82B512BCD626D0075EAF0 /* OtherSamplesList.swift in Sources */,
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */, 885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
E2D82B4F2BCD61590075EAF0 /* VitalsList.swift in Sources */,
E2E552A12BA4B14600BF5E9B /* HeartRateSample.swift in Sources */, E2E552A12BA4B14600BF5E9B /* HeartRateSample.swift in Sources */,
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */, E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
E2D82B2E2BCD319D0075EAF0 /* BodyMeasurementsList.swift in Sources */,
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */, E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
E2E552892BA2194400BF5E9B /* DatabasesTab.swift in Sources */, E2E552892BA2194400BF5E9B /* DatabasesTab.swift in Sources */,
E2D82B452BCD582D0075EAF0 /* MentalHealthList.swift in Sources */,
E2E552902BA236A000BF5E9B /* DatabaseList.swift in Sources */, E2E552902BA236A000BF5E9B /* DatabaseList.swift in Sources */,
E2E552B12BA98BE000BF5E9B /* MKMapRect+Extensions.swift in Sources */, E2E552B12BA98BE000BF5E9B /* MKMapRect+Extensions.swift in Sources */,
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */, 8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
E2E5528C2BA21C0700BF5E9B /* HealthDatabase.swift in Sources */, E2E5528C2BA21C0700BF5E9B /* Database.swift in Sources */,
E2D82B3D2BCD3C3F0075EAF0 /* HearingSamplesList.swift in Sources */,
E2E5528E2BA21C5900BF5E9B /* FileManager+Directory.swift in Sources */, E2E5528E2BA21C5900BF5E9B /* FileManager+Directory.swift in Sources */,
E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */, E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */,
E2E552B32BA9A1D600BF5E9B /* WorkoutTypeSelection.swift in Sources */, E2E552B32BA9A1D600BF5E9B /* WorkoutTypeSelection.swift in Sources */,
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */, E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
E2E552AB2BA859A700BF5E9B /* MetadataKey+String.swift in Sources */, E2E552AB2BA859A700BF5E9B /* MetadataKey+String.swift in Sources */,
E20881D52B76944A00D41D95 /* Test.swift in Sources */,
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
E2E552B52BA9A5D200BF5E9B /* WorkoutListRow.swift in Sources */, E2E552B52BA9A5D200BF5E9B /* WorkoutListRow.swift in Sources */,
E2D82B4B2BCD5E520075EAF0 /* RespiratorySamplesList.swift in Sources */,
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */, E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
E2E552AD2BA98B9B00BF5E9B /* RouteView.swift in Sources */, E2E552AD2BA98B9B00BF5E9B /* RouteView.swift in Sources */,
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */, 8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
@@ -616,14 +688,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.14.1;
};
};
885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */ = { 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git"; repositoryURL = "https://github.com/apple/swift-collections.git";
@@ -640,6 +704,22 @@
minimumVersion = 0.4.0; minimumVersion = 0.4.0;
}; };
}; };
E29E17BD2D4D04A200E0EE54 /* XCRemoteSwiftPackageReference "SQLite" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.15.3;
};
};
E29E17C02D4D04B900E0EE54 /* XCRemoteSwiftPackageReference "HealthDB" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/christophhagen/HealthDB";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.4.4;
};
};
E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
@@ -648,14 +728,6 @@
minimumVersion = 5.2.0; minimumVersion = 5.2.0;
}; };
}; };
E2E552972BA3748500BF5E9B /* XCRemoteSwiftPackageReference "HealthDB" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/christophhagen/HealthDB";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.3.0;
};
};
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-protobuf.git"; repositoryURL = "https://github.com/apple/swift-protobuf.git";
@@ -667,11 +739,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
885002762B5C2FC400E7D4DB /* SQLite */ = {
isa = XCSwiftPackageProductDependency;
package = 885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */;
productName = SQLite;
};
885002A52B5D296700E7D4DB /* Collections */ = { 885002A52B5D296700E7D4DB /* Collections */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */; package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */;
@@ -692,16 +759,21 @@
package = E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */; package = E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */;
productName = HealthKitExtensions; productName = HealthKitExtensions;
}; };
E29E17BE2D4D04A200E0EE54 /* SQLite */ = {
isa = XCSwiftPackageProductDependency;
package = E29E17BD2D4D04A200E0EE54 /* XCRemoteSwiftPackageReference "SQLite" */;
productName = SQLite;
};
E29E17C12D4D04B900E0EE54 /* HealthDB */ = {
isa = XCSwiftPackageProductDependency;
package = E29E17C02D4D04B900E0EE54 /* XCRemoteSwiftPackageReference "HealthDB" */;
productName = HealthDB;
};
E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */ = { E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; package = E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
productName = SFSafeSymbols; productName = SFSafeSymbols;
}; };
E2E5529D2BA47BA600BF5E9B /* HealthDB */ = {
isa = XCSwiftPackageProductDependency;
package = E2E552972BA3748500BF5E9B /* XCRemoteSwiftPackageReference "HealthDB" */;
productName = HealthDB;
};
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = { E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */; package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */;

View File

@@ -1,13 +1,13 @@
{ {
"originHash" : "5b8e27ff27b74293d3ae2085172fcc80a2317825fae6f3e7879caab9728af319", "originHash" : "853d27802fcd1b74fc9030c3fd718aa74e5a159d9bd4e67bca057ac38d56b364",
"pins" : [ "pins" : [
{ {
"identity" : "healthdb", "identity" : "healthdb",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/christophhagen/HealthDB", "location" : "https://github.com/christophhagen/HealthDB",
"state" : { "state" : {
"revision" : "6dfcafc66d59bc5887f7bfd66818a169cd7b73dd", "revision" : "b02391d12d52623fa15fa79a097c7fefd16ddf32",
"version" : "0.3.0" "version" : "0.4.4"
} }
}, },
{ {
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift", "location" : "https://github.com/stephencelis/SQLite.swift",
"state" : { "state" : {
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
"version" : "0.14.1" "version" : "0.15.3"
} }
}, },
{ {

View File

@@ -1,97 +0,0 @@
import SwiftUI
import HealthKit
import HealthKitExtensions
import HealthDB
import CoreLocation
struct ActivityDetailView: View {
@EnvironmentObject
var database: Database
let workout: Workout
let activity: HKWorkoutActivity
@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 {
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)
}
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(MetadataKeyName(key), value: "\(value)")
}
}
}
}
.navigationTitle("Activity")
.navigationDestination(for: [CLLocation].self) { locations in
LocationSampleListView(samples: locations)
}
.onAppear(perform: load)
}
private func load() {
guard let store = database.store else {
return
}
Task {
do {
guard let route = try store.route(associatedWith: workout) else {
return
}
let samples = try store.locations(associatedWith: route)
.sorted { $0.timestamp }
//let sampleCount = try HealthDatabase.shared.sampleCount(for: activity)
DispatchQueue.main.async {
self.locations = samples
//self.sampleCount = sampleCount
}
} catch {
print("Failed to load location samples for activity: \(error)")
}
}
}
}
#Preview {
NavigationStack {
ActivityDetailView(workout: .mock1, activity: .mock1)
.environmentObject(Database.mock)
}
}

View File

@@ -1,71 +0,0 @@
import SwiftUI
import OrderedCollections
import HealthKit
import HealthDB
struct ActivitySamplesView: View {
let activity: HKWorkoutActivity
@State var samples: [(type: HKSampleType, samples: [HKSample])] = []
@State var timeZones: [TimeZone] = []
init(activity: HKWorkoutActivity) {
self.activity = activity
}
var body: some View {
List {
if !timeZones.isEmpty {
Section("Time Zones") {
ForEach(timeZones, id: \.identifier) { timeZone in
Text(timeZone.debugDescription)
}
}
}
Section("Samples") {
ForEach(samples, id: \.0) { entry in
NavigationLink {
SampleListView(type: entry.type, samples: entry.samples)
} label: {
DetailRow(entry.type.description, value: entry.samples.count)
}
}
}
}.onAppear(perform: load)
}
private func load() {
Task {
self.loadAsync()
}
}
private func loadAsync() {
#warning("Load samples for activity")
/*
do {
let samples = try HealthDatabase.shared.samples(for: activity)
let ordered = samples
.sorted(using: { $0.key.rawValue })
.map { (type: $0, samples: $1) }
let timeZones: Set<TimeZone> = samples.reduce(into: Set()) { timeZones, sample in
timeZones.formUnion(sample.compactMap { $0.timeZone })
}
DispatchQueue.main.async {
self.samples = ordered
self.timeZones = timeZones.sorted { $0.identifier }
}
} catch {
print("Failed to load samples: \(error)")
}
*/
}
}
#Preview {
ActivitySamplesView(activity: .mock1)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

After

Width:  |  Height:  |  Size: 432 KiB

View File

@@ -1,7 +1,7 @@
import Foundation import Foundation
import HealthKit import HealthKit
extension HKWorkoutEvent: Identifiable { extension HKWorkoutEvent: @retroactive Identifiable {
public var id: Int { public var id: Int {
Int(dateInterval.start.timeIntervalSinceReferenceDate) << 16 + Int(dateInterval.start.timeIntervalSinceReferenceDate) << 16 +

View File

@@ -10,7 +10,7 @@ private let df: DateFormatter = {
return df return df
}() }()
extension Workout: Identifiable { extension Workout: @retroactive Identifiable {
public var id: Int { public var id: Int {
dataId dataId

View File

@@ -32,7 +32,6 @@ struct HealthImportApp: App {
DispatchQueue.main.async { DispatchQueue.main.async {
// Go back to main queue so that list will be updated // Go back to main queue so that list will be updated
guard let databaseToLoad = databaseList.databases.first(where: { $0.isDefault }) else { guard let databaseToLoad = databaseList.databases.first(where: { $0.isDefault }) else {
print("No default database to load")
return return
} }
Task { Task {
@@ -41,7 +40,6 @@ struct HealthImportApp: App {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
print("Setting selection to workouts")
self.selection = .workouts self.selection = .workouts
} }
} }

View File

@@ -3,20 +3,32 @@ import SQLite
import HealthKit import HealthKit
import HealthDB import HealthDB
extension Database { extension HealthDatabase {
private static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite") private static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite")
static var mock: Database { static var mock: HealthDatabase {
let bundleUrl = Database.databaseFileUrl! let bundleUrl = databaseFileUrl!
let local = FileManager.default.documentDirectory.appendingPathComponent("db.sqlite") let local = FileManager.default.documentDirectory.appendingPathComponent("db.sqlite")
if !FileManager.default.fileExists(atPath: local.path) { if !FileManager.default.fileExists(atPath: local.path) {
try! FileManager.default.copyItem(at: bundleUrl, to: local) try! FileManager.default.copyItem(at: bundleUrl, to: local)
} }
let store = try! HealthDatabase(fileUrl: local) return try! HealthDatabase(fileUrl: local)
return .init(store: store)
} }
static var empty: HealthDatabase {
let store = try! HKDatabaseStore(database: Connection(.inMemory))
try! store.createTables()
return .init(wrapping: store)
}
}
extension Database {
// static var mock: Database {
// return .init(store: .mock)
// }
static var empty: Database { static var empty: Database {
do { do {

View File

@@ -10,9 +10,9 @@ extension HKWorkoutEvent {
duration: 1114.56374406815), duration: 1114.56374406815),
metadata: [ metadata: [
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000), "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
"_HKPrivateWorkoutSegmentEventSubtype": NSNumber(1), "_HKPrivateWorkoutSegmentEventSubtype": NSNumber(value: UInt64(1)),
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000), "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(1), "_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(1)),
"_HKPrivateMetadataIsPartialSplit": NSNumber(0), "_HKPrivateMetadataIsPartialSplit": NSNumber(0),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1114.56) "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1114.56)
]), ]),
@@ -22,10 +22,10 @@ extension HKWorkoutEvent {
metadata: [ metadata: [
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17), "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17),
"_HKPrivateMetadataIsPartialSplit": 0, "_HKPrivateMetadataIsPartialSplit": NSNumber(value: UInt64(0)),
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateWorkoutSegmentEventSubtype": 1, "_HKPrivateWorkoutSegmentEventSubtype": NSNumber(value: UInt64(1)),
"_HKPrivateMetadataSplitMeasuringSystem": 2 "_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(2))
]), ]),
.init(type: .init(rawValue: 1)!, .init(type: .init(rawValue: 1)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113), dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
@@ -33,10 +33,10 @@ extension HKWorkoutEvent {
metadata: [ metadata: [
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), "_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17), "_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17),
"_HKPrivateMetadataIsPartialSplit": 0, "_HKPrivateMetadataIsPartialSplit": NSNumber(value: UInt64(0)),
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34), "_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateWorkoutSegmentEventSubtype": 1, "_HKPrivateWorkoutSegmentEventSubtype": NSNumber(value: UInt64(1)),
"_HKPrivateMetadataSplitMeasuringSystem": 2 "_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(2)),
]), ]),
.init(type: .init(rawValue: 2)!, .init(type: .init(rawValue: 2)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132), dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),

View File

@@ -1,30 +0,0 @@
import SwiftUI
import HealthKit
struct SampleListView: View {
let type: HKSampleType
let samples: [HKSample]
var body: some View {
List {
ForEach(samples) { sample in
DetailRow("", value: sample)
}
}
.navigationTitle(type.description)
}
}
/*
#Preview {
SampleListView()
}
*/
extension HKSample: Identifiable {
public var id: UUID {
uuid
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
import HealthDB
import HealthKit
import HealthKitExtensions
struct CategoryEmptySampleList<T>: View where T: HKCategoryEmptySample {
let database: HealthDatabase
@State
private var samples: [T] = []
var body: some View {
GenericSampleList(sampleView: { sample in
Text(sample.endDate.formatted())
}, search: { start, end, _ -> [T] in
try database.samples(from: start, to: end)
})
.navigationTitle(T.categoryTypeIdentifier.description)
}
}
#Preview {
NavigationStack {
CategoryEmptySampleList<MindfulSession>(database: .empty)
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
import HealthKitExtensions
import HealthDB
struct CategoryEmptySampleRow<T>: View where T: HKCategoryEmptySample {
let database: HealthDatabase
let title: String?
init(database: HealthDatabase, title: String? = nil) {
self.database = database
self.title = title
}
var body: some View {
NavigationLink {
CategoryEmptySampleList<T>(database: database)
} label: {
Text(title ?? T.categoryTypeIdentifier.description)
}
}
}
#Preview {
CategoryEmptySampleRow<MindfulSession>(database: .empty)
}

View File

@@ -0,0 +1,31 @@
import SwiftUI
import HealthDB
import HealthKit
import HealthKitExtensions
struct CategoryEnumSampleList<T>: View where T: HKCategoryEnumSample {
let database: HealthDatabase
@State
private var samples: [T] = []
var body: some View {
GenericSampleList(sampleView: { sample in
HStack {
Text("\(sample.value)")
Spacer()
Text(sample.endDate.formatted())
}
}, search: { start, end, _ -> [T] in
try database.samples(from: start, to: end)
})
.navigationTitle(T.categoryTypeIdentifier.description)
}
}
#Preview {
NavigationStack {
CategoryEnumSampleList<AbdominalCramps>(database: .empty)
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
import HealthKitExtensions
import HealthDB
struct CategoryEnumSampleRow<T>: View where T: HKCategoryEnumSample {
let database: HealthDatabase
let title: String?
init(database: HealthDatabase, title: String? = nil) {
self.database = database
self.title = title
}
var body: some View {
NavigationLink {
CategoryEnumSampleList<T>(database: database)
} label: {
Text(title ?? T.categoryTypeIdentifier.description)
}
}
}
#Preview {
CategoryEnumSampleRow<AbdominalCramps>(database: .empty)
}

View File

@@ -0,0 +1,82 @@
import SwiftUI
import HealthDB
import HealthKit
import HealthKitExtensions
struct GenericSampleList<Sample, Content>: View where Content: View, Sample: HKSampleContainer {
let sampleView: (Sample) -> Content
let search: (Date, Date, Bool) throws -> [Sample]
@State
private var samples: [Sample] = []
@State
private var startDate = Date.now.addingTimeInterval(-86400)
@State
private var endDate = Date.now
@State
private var sortAscending = false
var body: some View {
List {
Section("Search") {
DatePicker("Start", selection: $startDate)
.datePickerStyle(.compact)
DatePicker("End", selection: $endDate)
.datePickerStyle(.compact)
Toggle("Sort ascending", isOn: $sortAscending)
Button(action: load) {
HStack {
Spacer()
Text("Find samples")
Spacer()
}
}
}
Section("Samples") {
ForEach(samples, id: \.uuid) { sample in
sampleView(sample)
}
}
}.onAppear {
load()
}
}
func load() {
print("Finding samples from \(startDate.formatted()) to \(endDate.formatted()) for \(Sample.self)")
Task {
do {
try await loadSamplesAsync()
} catch {
print("Failed to load samples: \(error)")
}
}
}
private func loadSamplesAsync() async throws {
let samples = try search(startDate, endDate, sortAscending)
.sorted(ascending: false) { $0.endDate }
print("Finished loading \(samples.count) samples")
DispatchQueue.main.async {
self.samples = samples
}
}
}
#Preview {
NavigationStack {
GenericSampleList(sampleView: { sample in
HStack {
Text(sample.quantity.description)
Spacer()
Text(sample.endDate.formatted())
}
}, search: { (_, _, _) -> [ActiveEnergyBurned] in
[] })
}
}

View File

@@ -0,0 +1,44 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct ActivitySamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<ActiveEnergyBurned>(database: database)
QuantitySampleRow<AppleExerciseTime>(database: database)
QuantitySampleRow<AppleMoveTime>(database: database)
QuantitySampleRow<AppleStandTime>(database: database)
QuantitySampleRow<BasalEnergyBurned>(database: database)
QuantitySampleRow<CyclingCadence>(database: database)
QuantitySampleRow<CyclingFunctionalThresholdPower>(database: database)
QuantitySampleRow<CyclingPower>(database: database)
QuantitySampleRow<CyclingSpeed>(database: database)
QuantitySampleRow<DistanceCycling>(database: database)
QuantitySampleRow<DistanceDownhillSnowSports>(database: database)
QuantitySampleRow<FlightsClimbed>(database: database)
QuantitySampleRow<NikeFuel>(database: database)
QuantitySampleRow<PhysicalEffort>(database: database)
QuantitySampleRow<PushCount>(database: database)
QuantitySampleRow<RunningPower>(database: database)
QuantitySampleRow<RunningSpeed>(database: database)
QuantitySampleRow<StepCount>(database: database)
QuantitySampleRow<DistanceSwimming>(database: database)
QuantitySampleRow<SwimmingStrokeCount>(database: database)
QuantitySampleRow<UnderwaterDepth>(database: database)
QuantitySampleRow<DistanceWalkingRunning>(database: database)
QuantitySampleRow<DistanceWheelchair>(database: database)
}
.navigationTitle("Activity")
}
}
#Preview {
NavigationStack {
ActivitySamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,32 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct BodyMeasurementsList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<BasalBodyTemperature>(database: database)
QuantitySampleRow<BodyFatPercentage>(database: database)
QuantitySampleRow<BodyMass>(database: database)
QuantitySampleRow<BodyMassIndex>(database: database)
QuantitySampleRow<BodyTemperature>(database: database)
QuantitySampleRow<ElectrodermalActivity>(database: database)
QuantitySampleRow<Height>(database: database)
QuantitySampleRow<LeanBodyMass>(database: database)
Text("Vision prescription")
.foregroundStyle(.secondary)
QuantitySampleRow<WaistCircumference>(database: database)
QuantitySampleRow<AppleSleepingWristTemperature>(database: database, title: "Wrist temperature")
}
.navigationTitle("Body Measurements")
}
}
#Preview {
NavigationStack {
BodyMeasurementsList(database: .empty)
}
}

View File

@@ -0,0 +1,57 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct CycleTrackingList: View {
let database: HealthDatabase
var body: some View {
List {
CategoryEnumSampleRow<AbdominalCramps>(database: database)
CategoryEnumSampleRow<Acne>(database: database)
CategoryEnumSampleRow<AppetiteChanges>(database: database)
QuantitySampleRow<BasalBodyTemperature>(database: database)
CategoryEnumSampleRow<BladderIncontinence>(database: database)
CategoryEnumSampleRow<Bloating>(database: database)
CategoryEnumSampleRow<BreastPain>(database: database)
CategoryEnumSampleRow<CervicalMucusQuality>(database: database)
CategoryEnumSampleRow<Chills>(database: database)
CategoryEnumSampleRow<Constipation>(database: database)
CategoryEnumSampleRow<Contraceptive>(database: database)
CategoryEnumSampleRow<Diarrhea>(database: database)
CategoryEnumSampleRow<DrySkin>(database: database)
CategoryEnumSampleRow<Fatigue>(database: database)
CategoryEnumSampleRow<HairLoss>(database: database)
CategoryEnumSampleRow<Headache>(database: database)
CategoryEnumSampleRow<HotFlashes>(database: database)
CategoryEmptySampleRow<InfrequentMenstrualCycles>(database: database)
CategoryEmptySampleRow<IntermenstrualBleeding>(database: database)
CategoryEmptySampleRow<IrregularMenstrualCycles>(database: database)
CategoryEmptySampleRow<Lactation>(database: database)
CategoryEnumSampleRow<LowerBackPain>(database: database)
CategoryEnumSampleRow<MemoryLapse>(database: database)
CategoryEnumSampleRow<MenstrualFlow>(database: database)
CategoryEnumSampleRow<MoodChanges>(database: database)
CategoryEnumSampleRow<Nausea>(database: database)
CategoryEnumSampleRow<NightSweats>(database: database)
CategoryEnumSampleRow<OvulationTestResult>(database: database)
CategoryEnumSampleRow<PelvicPain>(database: database)
CategoryEmptySampleRow<PersistentIntermenstrualBleeding>(database: database)
CategoryEmptySampleRow<Pregnancy>(database: database)
CategoryEnumSampleRow<PregnancyTestResult>(database: database)
CategoryEnumSampleRow<ProgesteroneTestResult>(database: database)
CategoryEmptySampleRow<ProlongedMenstrualPeriods>(database: database)
CategoryEmptySampleRow<SexualActivity>(database: database)
CategoryEnumSampleRow<SleepChanges>(database: database)
CategoryEnumSampleRow<VaginalDryness>(database: database)
}
.navigationTitle("Cycle Tracking")
}
}
#Preview {
NavigationStack {
CycleTrackingList(database: .empty)
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct HearingSamplesList: View {
let database: HealthDatabase
var body: some View {
List {
Text("Audiogram")
.foregroundStyle(.secondary)
QuantitySampleRow<EnvironmentalAudioExposure>(database: database)
CategoryEnumSampleRow<EnvironmentalAudioExposureEvent>(database: database)
QuantitySampleRow<EnvironmentalSoundReduction>(database: database)
QuantitySampleRow<HeadphoneAudioExposure>(database: database)
CategoryEnumSampleRow<HeadphoneAudioExposureEvent>(database: database)
}
.navigationTitle("Hearing")
}
}
#Preview {
NavigationStack {
HearingSamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,39 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct HeartSamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<HeartRate>(database: database)
QuantitySampleRow<HeartRateVariabilitySDNN>(database: database, title: "Heart Rate Variability")
QuantitySampleRow<RestingHeartRate>(database: database)
QuantitySampleRow<WalkingHeartRateAverage>(database: database)
QuantitySampleRow<Vo2Max>(database: database, title: "Cardio Fitness")
CategoryEmptySampleRow<LowHeartRateEvent>(database: database)
Text("Electrocardiograms (ECG)")
.foregroundStyle(.secondary)
#warning("Create view for Electrocardiograms")
QuantitySampleRow<AtrialFibrillationBurden>(database: database)
Text("Blood Pressure")
.foregroundStyle(.secondary)
#warning("Create view for blood pressure")
QuantitySampleRow<BloodPressureDiastolic>(database: database)
QuantitySampleRow<BloodPressureSystolic>(database: database)
CategoryEnumSampleRow<LowCardioFitnessEvent>(database: database)
CategoryEmptySampleRow<HighHeartRateEvent>(database: database)
CategoryEmptySampleRow<IrregularHeartRhythmEvent>(database: database)
QuantitySampleRow<PeripheralPerfusionIndex>(database: database)
}
.navigationTitle("Heart")
}
}
#Preview {
NavigationStack {
HeartSamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,31 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct MentalHealthList: View {
let database: HealthDatabase
var body: some View {
List {
Text("Anxiety Risk")
.foregroundStyle(.secondary)
Text("Depression Risk")
.foregroundStyle(.secondary)
QuantitySampleRow<AppleExerciseTime>(database: database, title: "Exercise Minutes")
CategoryEmptySampleRow<MindfulSession>(database: database, title: "Mindful Minutes")
Text("Sleep")
.foregroundStyle(.secondary)
Text("State of Mind")
.foregroundStyle(.secondary)
QuantitySampleRow<TimeInDaylight>(database: database)
}
.navigationTitle("Mental Wellbeing")
}
}
#Preview {
NavigationStack {
MentalHealthList(database: .empty)
}
}

View File

@@ -0,0 +1,33 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct MobilitySamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<Vo2Max>(database: database, title: "Cardio Fitness")
QuantitySampleRow<WalkingDoubleSupportPercentage>(database: database, title: "Double Support Time")
QuantitySampleRow<RunningGroundContactTime>(database: database, title: "Ground Contact Time")
QuantitySampleRow<RunningStrideLength>(database: database)
QuantitySampleRow<SixMinuteWalkTestDistance>(database: database, title: "Six-Minute Walk")
QuantitySampleRow<StairDescentSpeed>(database: database, title: "Stair Speed: Down")
QuantitySampleRow<StairAscentSpeed>(database: database, title: "Stair Speed: Up")
QuantitySampleRow<RunningVerticalOscillation>(database: database, title: "Vertical Oscillation")
QuantitySampleRow<WalkingAsymmetryPercentage>(database: database, title: "Walking Asymmetry")
QuantitySampleRow<WalkingSpeed>(database: database)
QuantitySampleRow<AppleWalkingSteadiness>(database: database, title: "Walking Steadiness")
QuantitySampleRow<WalkingStepLength>(database: database)
CategoryEnumSampleRow<AppleWalkingSteadinessEvent>(database: database, title: "Walking Steadiness Notifications")
}
.navigationTitle("Mobility")
}
}
#Preview {
NavigationStack {
MobilitySamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,59 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct NutritionSamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<DietaryBiotin>(database: database, title: "Biotin")
QuantitySampleRow<DietaryCaffeine>(database: database, title: "Caffeine")
QuantitySampleRow<DietaryCalcium>(database: database, title: "Calcium")
QuantitySampleRow<DietaryCarbohydrates>(database: database, title: "Carbohydrates")
QuantitySampleRow<DietaryChloride>(database: database, title: "Chloride")
QuantitySampleRow<DietaryChromium>(database: database, title: "Chromium")
QuantitySampleRow<DietaryCopper>(database: database, title: "Copper")
QuantitySampleRow<DietaryCholesterol>(database: database)
QuantitySampleRow<DietaryEnergyConsumed>(database: database, title: "Dietary Energy")
QuantitySampleRow<DietarySugar>(database: database)
QuantitySampleRow<DietaryFiber>(database: database, title: "Fiber")
QuantitySampleRow<DietaryFolate>(database: database, title: "Folate")
QuantitySampleRow<DietaryIodine>(database: database, title: "Iodine")
QuantitySampleRow<DietaryIron>(database: database, title: "Iron")
QuantitySampleRow<DietaryMagnesium>(database: database, title: "Magnesium")
QuantitySampleRow<DietaryManganese>(database: database, title: "Manganese")
QuantitySampleRow<DietaryMolybdenum>(database: database, title: "Molybdenum")
QuantitySampleRow<DietaryFatMonounsaturated>(database: database, title: "Monounsaturated Fat")
QuantitySampleRow<DietaryNiacin>(database: database, title: "Niacin")
QuantitySampleRow<DietaryPantothenicAcid>(database: database, title: "Pantothenic Acid")
QuantitySampleRow<DietaryPhosphorus>(database: database, title: "Phosphorus")
QuantitySampleRow<DietaryFatPolyunsaturated>(database: database, title: "Polyunsaturated Fat")
QuantitySampleRow<DietaryPotassium>(database: database, title: "Potassium")
QuantitySampleRow<DietaryProtein>(database: database, title: "Protein")
QuantitySampleRow<DietaryRiboflavin>(database: database, title: "Riboflavin")
QuantitySampleRow<DietaryFatSaturated>(database: database, title: "Saturated Fat")
QuantitySampleRow<DietarySelenium>(database: database, title: "Selenium")
QuantitySampleRow<DietarySodium>(database: database, title: "Sodium")
QuantitySampleRow<DietaryThiamin>(database: database, title: "Thiamine")
QuantitySampleRow<DietaryFatTotal>(database: database, title: "Total Fat")
QuantitySampleRow<DietaryVitaminA>(database: database, title: "Vitamin A")
QuantitySampleRow<DietaryVitaminB6>(database: database, title: "Vitamin B6")
QuantitySampleRow<DietaryVitaminB12>(database: database, title: "Vitamin B12")
QuantitySampleRow<DietaryVitaminC>(database: database, title: "Vitamin C")
QuantitySampleRow<DietaryVitaminD>(database: database, title: "Vitamin D")
QuantitySampleRow<DietaryVitaminE>(database: database, title: "Vitamin E")
QuantitySampleRow<DietaryVitaminK>(database: database, title: "Vitamin K")
QuantitySampleRow<DietaryWater>(database: database, title: "Water")
QuantitySampleRow<DietaryZinc>(database: database, title: "Zinc")
}
.navigationTitle("Nutrition")
}
}
#Preview {
NavigationStack {
NutritionSamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,31 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct OtherSamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<BloodAlcoholContent>(database: database)
QuantitySampleRow<BloodGlucose>(database: database)
CategoryEmptySampleRow<HandwashingEvent>(database: database)
QuantitySampleRow<InhalerUsage>(database: database)
QuantitySampleRow<InsulinDelivery>(database: database)
QuantitySampleRow<NumberOfTimesFallen>(database: database, title: "Number of Times Fallen")
CategoryEmptySampleRow<SexualActivity>(database: database)
QuantitySampleRow<TimeInDaylight>(database: database)
CategoryEmptySampleRow<ToothbrushingEvent>(database: database)
QuantitySampleRow<UvExposure>(database: database, title: "UV Index")
QuantitySampleRow<WaterTemperature>(database: database)
}
.navigationTitle("Other Data")
}
}
#Preview {
NavigationStack {
OtherSamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,28 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct RespiratorySamplesList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<OxygenSaturation>(database: database, title: "Blood Oxygen")
QuantitySampleRow<Vo2Max>(database: database, title: "Cardio Fitness")
QuantitySampleRow<ForcedExpiratoryVolume1>(database: database, title: "Forced Expiratory Volume, 1 sec")
QuantitySampleRow<ForcedVitalCapacity>(database: database)
QuantitySampleRow<InhalerUsage>(database: database)
QuantitySampleRow<PeakExpiratoryFlowRate>(database: database)
QuantitySampleRow<RespiratoryRate>(database: database)
QuantitySampleRow<SixMinuteWalkTestDistance>(database: database, title: "Six Minute Walk Distance")
}
.navigationTitle("Respiratory")
}
}
#Preview {
NavigationStack {
RespiratorySamplesList(database: .empty)
}
}

View File

@@ -0,0 +1,57 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct SymptomsList: View {
let database: HealthDatabase
var body: some View {
List {
CategoryEnumSampleRow<AbdominalCramps>(database: database)
CategoryEnumSampleRow<Acne>(database: database)
CategoryEnumSampleRow<AppetiteChanges>(database: database)
CategoryEnumSampleRow<BladderIncontinence>(database: database)
CategoryEnumSampleRow<Bloating>(database: database)
CategoryEnumSampleRow<GeneralizedBodyAche>(database: database, title: "Body and Muscle Ache")
CategoryEnumSampleRow<BreastPain>(database: database)
CategoryEnumSampleRow<ChestTightnessOrPain>(database: database)
CategoryEnumSampleRow<Chills>(database: database)
CategoryEnumSampleRow<SinusCongestion>(database: database, title: "Congestion")
CategoryEnumSampleRow<Constipation>(database: database)
CategoryEnumSampleRow<Coughing>(database: database)
CategoryEnumSampleRow<Diarrhea>(database: database, title: "Diarrhoea")
CategoryEnumSampleRow<DrySkin>(database: database)
CategoryEnumSampleRow<Fainting>(database: database)
CategoryEnumSampleRow<Fatigue>(database: database)
CategoryEnumSampleRow<Fever>(database: database)
CategoryEnumSampleRow<HairLoss>(database: database)
CategoryEnumSampleRow<Headache>(database: database)
CategoryEnumSampleRow<Heartburn>(database: database)
CategoryEnumSampleRow<HotFlashes>(database: database)
CategoryEnumSampleRow<LossOfSmell>(database: database)
CategoryEnumSampleRow<LossOfTaste>(database: database)
CategoryEnumSampleRow<LowerBackPain>(database: database)
CategoryEnumSampleRow<MemoryLapse>(database: database)
CategoryEnumSampleRow<MoodChanges>(database: database)
CategoryEnumSampleRow<Nausea>(database: database)
CategoryEnumSampleRow<NightSweats>(database: database)
CategoryEnumSampleRow<PelvicPain>(database: database)
CategoryEnumSampleRow<RapidPoundingOrFlutteringHeartbeat>(database: database)
CategoryEnumSampleRow<RunnyNose>(database: database)
CategoryEnumSampleRow<ShortnessOfBreath>(database: database)
CategoryEnumSampleRow<SleepChanges>(database: database)
CategoryEnumSampleRow<SoreThroat>(database: database)
CategoryEnumSampleRow<VaginalDryness>(database: database)
CategoryEnumSampleRow<Vomiting>(database: database)
CategoryEnumSampleRow<Wheezing>(database: database)
}
.navigationTitle("Symptoms")
}
}
#Preview {
NavigationStack {
SymptomsList(database: .empty)
}
}

View File

@@ -0,0 +1,29 @@
import SwiftUI
import HealthDB
import HealthKitExtensions
struct VitalsList: View {
let database: HealthDatabase
var body: some View {
List {
QuantitySampleRow<BloodGlucose>(database: database)
QuantitySampleRow<OxygenSaturation>(database: database, title: "Blood Oxygen")
Text("Blood Pressure")
.foregroundStyle(.secondary)
QuantitySampleRow<BodyTemperature>(database: database)
QuantitySampleRow<HeartRate>(database: database)
CategoryEnumSampleRow<MenstrualFlow>(database: database)
QuantitySampleRow<RespiratoryRate>(database: database)
}
.navigationTitle("Vitals")
}
}
#Preview {
NavigationStack {
VitalsList(database: .empty)
}
}

View File

@@ -0,0 +1,28 @@
import SwiftUI
import HealthDB
import HealthKit
import HealthKitExtensions
struct QuantitySampleList<T>: View where T: HKQuantitySampleContainer {
let database: HealthDatabase
var body: some View {
GenericSampleList(sampleView: { sample in
HStack {
Text(sample.quantity.description)
Spacer()
Text(sample.endDate.formatted())
}
}, search: { start, end, _ -> [T] in
try database.samples(includingSeriesData: true, from: start, to: end)
})
.navigationTitle(T.quantityTypeIdentifier.description)
}
}
#Preview {
NavigationStack {
QuantitySampleList<ActiveEnergyBurned>(database: .empty)
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
import HealthKitExtensions
import HealthDB
struct QuantitySampleRow<T>: View where T: HKQuantitySampleContainer {
let database: HealthDatabase
let title: String?
init(database: HealthDatabase, title: String? = nil) {
self.database = database
self.title = title
}
var body: some View {
NavigationLink {
QuantitySampleList<T>(database: database)
} label: {
Text(title ?? T.quantityTypeIdentifier.description)
}
}
}
#Preview {
QuantitySampleRow<ActiveEnergyBurned>(database: .empty)
}

View File

@@ -83,7 +83,7 @@ struct SearchHealthStoreView: View {
SearchHealthStoreView() SearchHealthStoreView()
} }
extension HKQuantityTypeIdentifier: Identifiable { extension HKQuantityTypeIdentifier: @retroactive Identifiable {
public var id: String { public var id: String {
rawValue rawValue

View File

@@ -8,21 +8,73 @@ struct SamplesTab: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List { List {
Label("Activity", systemSymbol: .flame) if let db = database.store {
Label("Body Measurements", systemSymbol: .figure) NavigationLink {
Label("Cycle Tracking", systemSymbol: .circleHexagonpath) ActivitySamplesList(database: db)
Label("Hearing", systemSymbol: .ear) } label: {
Label("Heart", systemSymbol: .heartFill) Label("Activity", systemSymbol: .flame)
Label("Medications", systemSymbol: .pills)
Label("Mental Wellbeing", systemSymbol: .brainHeadProfile)
Label("Mobility", systemSymbol: .arrowLeftAndRight)
Label("Nutrition", systemSymbol: .carrot)
Label("Respiratory", systemSymbol: .lungs)
Label("Sleep", systemSymbol: .bedDouble)
Label("Symptoms", systemSymbol: .listBulletClipboard)
Label("Vitals", systemSymbol: .waveformPathEcgRectangle)
Label("Other Data", systemSymbol: .cross)
}
NavigationLink {
BodyMeasurementsList(database: db)
} label: {
Label("Body Measurements", systemSymbol: .figure)
}
Label("Clinical documents", systemSymbol: .listClipboard)
.foregroundColor(.secondary)
NavigationLink {
CycleTrackingList(database: db)
} label: {
Label("Cycle Tracking", systemSymbol: .circleHexagonpath)
}
NavigationLink {
HearingSamplesList(database: db)
} label: {
Label("Hearing", systemSymbol: .ear)
}
NavigationLink {
HeartSamplesList(database: db)
} label: {
Label("Heart", systemSymbol: .heart)
}
Label("Medications", systemSymbol: .pills)
.foregroundColor(.secondary)
NavigationLink {
MentalHealthList(database: db)
} label: {
Label("Mental Wellbeing", systemSymbol: .brainHeadProfile)
}
NavigationLink {
MobilitySamplesList(database: db)
} label: {
Label("Mobility", systemSymbol: .arrowLeftAndRight)
}
NavigationLink {
NutritionSamplesList(database: db)
} label: {
Label("Nutrition", systemSymbol: .carrot)
}
NavigationLink {
RespiratorySamplesList(database: db)
} label: {
Label("Respiratory", systemSymbol: .lungs)
}
Label("Sleep", systemSymbol: .bedDouble)
.foregroundColor(.secondary)
NavigationLink {
SymptomsList(database: db)
} label: {
Label("Symptoms", systemSymbol: .listBulletClipboard)
}
NavigationLink {
VitalsList(database: db)
} label: {
Label("Vitals", systemSymbol: .waveformPathEcgRectangle)
}
Label("Other Data", systemSymbol: .cross)
} else {
Text("No database loaded")
}
} }
.navigationTitle("Health") .navigationTitle("Health")
} }
@@ -30,6 +82,6 @@ struct SamplesTab: View {
} }
#Preview { #Preview {
SamplesTab(database: Database()) SamplesTab(database: Database.empty)
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
} }

View File

@@ -146,26 +146,5 @@ struct WorkoutTab: View {
#Preview { #Preview {
WorkoutTab() WorkoutTab()
.environmentObject(Database.mock) .environmentObject(Database.empty)
} }
/*
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
*/

View File

@@ -1,227 +0,0 @@
import Foundation
import HealthKit
import HealthKitExtensions
func insertExamplesOfAllTypes() async throws {
let store = HealthStore()
guard try await requestAllPermissions(in: store) else {
return
}
var startDate = Date(timeIntervalSinceReferenceDate: 700_000_000)
try await insertCategoryTypes(in: store, startDate: &startDate)
try await insertQuantityTypes(in: store, startDate: &startDate)
}
func requestAllPermissions(in store: HealthStore) async throws -> Bool {
let writable: [HKSampleContainer.Type] = HKQuantityType.writableTypes + HKCorrelationType.writableTypes + HKCategoryType.writableTypes
let readable: [HKObjectContainer.Type] = HKQuantityType.readableTypes + HKCorrelationType.readableTypes + HKCategoryType.readableTypes
try await store.requestAuthorization(toShare: writable, read: readable)
var hasAllPermissions = true
writable.forEach {
if store.authorizationStatus(for: $0) != .sharingAuthorized {
print("Missing permission for \($0.objectType)")
hasAllPermissions = false
}
}
return hasAllPermissions
}
private func insertCategoryTypes(in store: HealthStore, startDate: inout Date) async throws {
func make<T>(convert: (Date, Date) -> T) -> T where T: HKObjectContainer {
let result = convert(startDate, startDate.addingTimeInterval(1))
print("\(startDate.timeIntervalSinceReferenceDate): \(T.objectType)")
startDate.addTimeInterval(1)
return result
}
let categorySamples: [HKCategorySampleContainer] = [
make { MindfulSession(start: $0, end: $1) },
make { HandwashingEvent(start: $0, end: $1) },
make { ToothbrushingEvent(start: $0, end: $1) },
make { CervicalMucusQuality(value: .creamy, start: $0, end: $1) },
make { Contraceptive(value: .implant, start: $0, end: $1) },
make { IntermenstrualBleeding(start: $0, end: $1) },
make { Lactation(start: $0, end: $1) },
make { MenstrualFlow(value: .heavy, cycleStart: true, start: $0, end: $1) },
make { OvulationTestResult(value: .negative, start: $0, end: $1) },
make { Pregnancy(start: $0, end: $1) },
make { PregnancyTestResult(value: .positive, start: $0, end: $1) },
make { SexualActivity(protectionUsed: true, start: $0, end: $1) },
make { SleepAnalysis(value: .asleepREM, start: $0, end: $1) },
make { AbdominalCramps(value: .moderate, start: $0, end: $1) },
make { Acne(value: .moderate, start: $0, end: $1) },
make { AppetiteChanges(value: .decreased, start: $0, end: $1) },
make { BladderIncontinence(value: .moderate, start: $0, end: $1) },
make { Bloating(value: .moderate, start: $0, end: $1) },
make { BreastPain(value: .moderate, start: $0, end: $1) },
make { ChestTightnessOrPain(value: .moderate, start: $0, end: $1) },
make { Chills(value: .moderate, start: $0, end: $1) },
make { Constipation(value: .moderate, start: $0, end: $1) },
make { Coughing(value: .moderate, start: $0, end: $1) },
make { Diarrhea(value: .moderate, start: $0, end: $1) },
make { Dizziness(value: .moderate, start: $0, end: $1) },
make { DrySkin(value: .moderate, start: $0, end: $1) },
make { Fainting(value: .moderate, start: $0, end: $1) },
make { Fatigue(value: .moderate, start: $0, end: $1) },
make { Fever(value: .moderate, start: $0, end: $1) },
make { GeneralizedBodyAche(value: .moderate, start: $0, end: $1) },
make { HairLoss(value: .moderate, start: $0, end: $1) },
make { Headache(value: .moderate, start: $0, end: $1) },
make { Heartburn(value: .moderate, start: $0, end: $1) },
make { HotFlashes(value: .moderate, start: $0, end: $1) },
make { LossOfSmell(value: .moderate, start: $0, end: $1) },
make { LossOfTaste(value: .moderate, start: $0, end: $1) },
make { LowerBackPain(value: .moderate, start: $0, end: $1) },
make { MemoryLapse(value: .moderate, start: $0, end: $1) },
make { MoodChanges(value: .present, start: $0, end: $1) },
make { Nausea(value: .moderate, start: $0, end: $1) },
make { NightSweats(value: .moderate, start: $0, end: $1) },
make { PelvicPain(value: .moderate, start: $0, end: $1) },
make { RapidPoundingOrFlutteringHeartbeat(value: .moderate, start: $0, end: $1) },
make { RunnyNose(value: .moderate, start: $0, end: $1) },
make { ShortnessOfBreath(value: .moderate, start: $0, end: $1) },
make { SinusCongestion(value: .moderate, start: $0, end: $1) },
make { SkippedHeartbeat(value: .moderate, start: $0, end: $1) },
make { SleepChanges(value: .present, start: $0, end: $1) },
make { SoreThroat(value: .moderate, start: $0, end: $1) },
make { VaginalDryness(value: .moderate, start: $0, end: $1) },
make { Vomiting(value: .moderate, start: $0, end: $1) },
make { Wheezing(value: .moderate, start: $0, end: $1) },
]
print("Saving...")
try await store.save(categorySamples)
print("Done.")
}
private func insertQuantityTypes(in store: HealthStore, startDate: inout Date) async throws {
func make<T>(convert: (Date, Date) -> T) -> T where T: HKObjectContainer {
let result = convert(startDate, startDate.addingTimeInterval(1))
print("\(startDate.timeIntervalSinceReferenceDate): \(T.objectType)")
startDate.addTimeInterval(1)
return result
}
let samples: [HKQuantitySampleContainer] = [
make { BodyFatPercentage(value: 10.0, start: $0, end: $1) },
make { BodyMass(value: 80.0, start: $0, end: $1) },
make { BodyMassIndex(value: 25.0, start: $0, end: $1) },
make { ElectrodermalActivity(value: 6.0, start: $0, end: $1) },
make { Height(value: 1.80, start: $0, end: $1) },
make { LeanBodyMass(value: 65.0, start: $0, end: $1) },
make { WaistCircumference(value: 1.0, start: $0, end: $1) },
make { ActiveEnergyBurned(value: 1.0, start: $0, end: $1) },
make { BasalEnergyBurned(value: 1.0, start: $0, end: $1) },
//make { CyclingCadence(value: 1.0, start: $0, end: $1) },
//make { CyclingFunctionalThresholdPower(value: 1.0, start: $0, end: $1) },
//make { CyclingPower(value: 1.0, start: $0, end: $1) },
//make { CyclingSpeed(value: 1.0, start: $0, end: $1) },
make { DistanceCycling(value: 1.0, start: $0, end: $1) },
make { DistanceDownhillSnowSports(value: 1.0, start: $0, end: $1) },
make { DistanceSwimming(value: 1.0, start: $0, end: $1) },
make { DistanceWalkingRunning(value: 1.0, start: $0, end: $1) },
make { DistanceWheelchair(value: 1.0, start: $0, end: $1) },
make { FlightsClimbed(value: 1.0, start: $0, end: $1) },
//make { PhysicalEffort(value: 1.0, start: $0, end: $1) },
make { PushCount(value: 1.0, start: $0, end: $1) },
make { RunningPower(value: 1.0, start: $0, end: $1) },
make { RunningSpeed(value: 1.0, start: $0, end: $1) },
make { StepCount(value: 1.0, start: $0, end: $1) },
make { SwimmingStrokeCount(value: 1.0, start: $0, end: $1) },
make { UnderwaterDepth(value: 1.0, start: $0, end: $1) },
make { EnvironmentalAudioExposure(value: 1.0, start: $0, end: $1) },
make { EnvironmentalSoundReduction(value: 1.0, start: $0, end: $1) },
make { HeadphoneAudioExposure(value: 1.0, start: $0, end: $1) },
make { HeartRate(countsPerSecond: 1.0, motionContext: .sedentary, start: $0, end: $1) },
make { HeartRateRecoveryOneMinute(value: 1.0, start: $0, end: $1) },
make { HeartRateVariabilitySDNN(value: 1.0, start: $0, end: $1) },
make { PeripheralPerfusionIndex(value: 1.0, start: $0, end: $1) },
make { RestingHeartRate(value: 1.0, start: $0, end: $1) },
make { Vo2Max(value: 1.0, testType: .maxExercise, start: $0, end: $1) },
make { RunningGroundContactTime(value: 1.0, start: $0, end: $1) },
make { RunningStrideLength(value: 1.0, start: $0, end: $1) },
make { RunningVerticalOscillation(value: 1.0, start: $0, end: $1) },
make { SixMinuteWalkTestDistance(value: 1.0, start: $0, end: $1) },
make { StairAscentSpeed(value: 1.0, start: $0, end: $1) },
make { StairDescentSpeed(value: 1.0, start: $0, end: $1) },
make { WalkingDoubleSupportPercentage(value: 1.0, start: $0, end: $1) },
make { WalkingSpeed(value: 1.0, start: $0, end: $1) },
make { WalkingStepLength(value: 1.0, start: $0, end: $1) },
make { DietaryBiotin(value: 1.0, start: $0, end: $1) },
make { DietaryCaffeine(value: 1.0, start: $0, end: $1) },
make { DietaryCalcium(value: 1.0, start: $0, end: $1) },
make { DietaryCarbohydrates(value: 1.0, start: $0, end: $1) },
make { DietaryChloride(value: 1.0, start: $0, end: $1) },
make { DietaryCholesterol(value: 1.0, start: $0, end: $1) },
make { DietaryChromium(value: 1.0, start: $0, end: $1) },
make { DietaryCopper(value: 1.0, start: $0, end: $1) },
make { DietaryEnergyConsumed(value: 1.0, start: $0, end: $1) },
make { DietaryFatMonounsaturated(value: 1.0, start: $0, end: $1) },
make { DietaryFatPolyunsaturated(value: 1.0, start: $0, end: $1) },
make { DietaryFatSaturated(value: 1.0, start: $0, end: $1) },
make { DietaryFatTotal(value: 1.0, start: $0, end: $1) },
make { DietaryFiber(value: 1.0, start: $0, end: $1) },
make { DietaryFolate(value: 1.0, start: $0, end: $1) },
make { DietaryIodine(value: 1.0, start: $0, end: $1) },
make { DietaryIron(value: 1.0, start: $0, end: $1) },
make { DietaryMagnesium(value: 1.0, start: $0, end: $1) },
make { DietaryManganese(value: 1.0, start: $0, end: $1) },
make { DietaryMolybdenum(value: 1.0, start: $0, end: $1) },
make { DietaryNiacin(value: 1.0, start: $0, end: $1) },
make { DietaryPantothenicAcid(value: 1.0, start: $0, end: $1) },
make { DietaryPhosphorus(value: 1.0, start: $0, end: $1) },
make { DietaryPotassium(value: 1.0, start: $0, end: $1) },
make { DietaryProtein(value: 1.0, start: $0, end: $1) },
make { DietaryRiboflavin(value: 1.0, start: $0, end: $1) },
make { DietarySelenium(value: 1.0, start: $0, end: $1) },
make { DietarySodium(value: 1.0, start: $0, end: $1) },
make { DietarySugar(value: 1.0, start: $0, end: $1) },
make { DietaryThiamin(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminA(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminB6(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminB12(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminC(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminD(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminE(value: 1.0, start: $0, end: $1) },
make { DietaryVitaminK(value: 1.0, start: $0, end: $1) },
make { DietaryWater(value: 1.0, start: $0, end: $1) },
make { DietaryZinc(value: 1.0, start: $0, end: $1) },
make { BloodPressureDiastolic(value: 1.0, start: $0, end: $1) },
make { BloodPressureSystolic(value: 1.0, start: $0, end: $1) },
make { InsulinDelivery(amount: 1.0, reason: .basal, start: $0, end: $1) },
make { NumberOfAlcoholicBeverages(value: 1.0, start: $0, end: $1) },
make { NumberOfTimesFallen(value: 1.0, start: $0, end: $1) },
//make { TimeInDaylight(value: 1.0, start: $0, end: $1) },
make { UvExposure(value: 1.0, start: $0, end: $1) },
make { WaterTemperature(value: 1.0, start: $0, end: $1) },
make { BasalBodyTemperature(value: 1.0, start: $0, end: $1) },
make { ForcedExpiratoryVolume1(value: 1.0, start: $0, end: $1) },
make { ForcedVitalCapacity(value: 1.0, start: $0, end: $1) },
make { InhalerUsage(value: 1.0, start: $0, end: $1) },
make { OxygenSaturation(value: 1.0, start: $0, end: $1) },
make { PeakExpiratoryFlowRate(value: 1.0, start: $0, end: $1) },
make { RespiratoryRate(value: 1.0, start: $0, end: $1) },
make { BloodGlucose(value: 1.0, start: $0, end: $1) },
make { BodyTemperature(value: 1.0, start: $0, end: $1) },
]
let allowed = Set(HKQuantityType.writableTypes.map { $0.quantitySampleType })
samples.forEach {
if !allowed.contains($0.quantitySampleType) {
print("Can't write: \($0.quantitySampleType)")
}
}
print("Saving...")
try await store.save(samples)
print("Done.")
}

View File

@@ -0,0 +1,125 @@
import SwiftUI
import HealthKit
import HealthKitExtensions
import HealthDB
import CoreLocation
struct ActivityDetailView: View {
@EnvironmentObject
var database: Database
let workout: Workout
let activity: HKWorkoutActivity
@State
private var statistics: [(type: HKQuantityType, statistics: Statistics)] = []
@State
private var privateMetadata: [(key: String, value: Any)] = []
@State
private var showErrorMessage = false
@State
private var errorMessage: String = ""
private var metadata: [(key: String, value: Any)] {
activity.metadata?.sorted { $0.key } ?? []
}
var body: some View {
List {
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 !statistics.isEmpty {
Section("Statistics") {
ForEach(statistics, id: \.type) { (type, statistic) in
let name = HKQuantityTypeIdentifier(rawValue: type.identifier).description
DetailRow(name, value: statistic.average)
}
}
}
if !(activity.metadata?.isEmpty ?? true) {
Section("Metadata") {
ForEach(metadata, id: \.key) { (key, value) in
DetailRow(MetadataKeyName(key), value: "\(value)")
}
}
}
if !privateMetadata.isEmpty {
Section("Private Metadata") {
ForEach(privateMetadata, id:\.key) { (key, value) in
DetailRow(MetadataKeyName(key), value: "\(value)")
}
}
}
}
.navigationTitle("Activity")
.onAppear(perform: load)
.alert("Processing error", isPresented: $showErrorMessage) {
Button("Dismiss", role: .cancel) { }
} message: {
Text(errorMessage)
}
}
private func show(_ error: String) {
DispatchQueue.main.async {
self.errorMessage = error
self.showErrorMessage = true
}
}
private func load() {
guard let store = database.store else {
return
}
Task {
await loadStatistics(db: store)
await loadPrivateMetadata(db: store)
}
}
private func loadStatistics(db: HealthDatabase) async {
do {
let statistics = try db.store.statistics(associatedWith: activity)
DispatchQueue.main.async {
self.statistics = statistics
.sorted { $0.key.description }
.map { ($0, $1) }
}
} catch {
print("Failed to load statistics from database: \(error)")
}
}
private func loadPrivateMetadata(db: HealthDatabase) async {
do {
let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true)
.filter { $0.key.hasPrefix("_HKPrivate") }
DispatchQueue.main.async {
self.privateMetadata = metadata.sorted { $0.key }
}
} catch {
show("Failed to load private metadata from database: \(error)")
}
}
}
#Preview {
NavigationStack {
ActivityDetailView(workout: .mock1, activity: .mock1)
.environmentObject(Database.empty)
}
}

View File

@@ -29,19 +29,24 @@ struct WorkoutDetailView: View {
private var locationSamples: [CLLocation] = [] private var locationSamples: [CLLocation] = []
@State @State
private var privateMetadata: [String : Any] = [:] private var privateMetadata: [(key: String, value: Any)] = []
private var metadataFields: [(key: String, value: Any)] { @State
workout.metadata.sorted { $0.key } private var showErrorMessage = false
}
private var privateMetadataFields: [(key: String, value: Any)] { @State
privateMetadata.sorted { $0.key } private var errorMessage: String = ""
}
@State @State
private var isProcessingWorkout = false private var isProcessingWorkout = false
@State
private var healthButtonText = "Checking for workout in Health..."
private var metadataFields: [(key: String, value: Any)] {
workout.metadata?.sorted { $0.key } ?? []
}
private var averageHeartRate: Int { private var averageHeartRate: Int {
let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute } let sum = heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute }
return (Double(sum) / Double(heartRateSamples.count)).roundedInt return (Double(sum) / Double(heartRateSamples.count)).roundedInt
@@ -49,37 +54,28 @@ struct WorkoutDetailView: View {
var body: some View { var body: some View {
List { List {
if healthWorkout != nil { Button(action: addOrDeleteHealthWorkout) {
HStack { HStack {
Spacer() Spacer()
if isProcessingWorkout {
ProgressView()
.progressViewStyle(.circular)
.padding(.trailing, 10)
}
VStack { VStack {
Text("Matching workout found in Health") Text(healthButtonText)
.foregroundStyle(.black) .foregroundStyle(.accent)
if healthWorkout != nil {
Text("Tap to delete")
.font(.caption)
.foregroundStyle(.gray)
}
} }
Spacer() Spacer()
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.listRowBackground(Color.accentColor)
} else {
Button(action: addWorkoutToHealth) {
HStack {
Spacer()
if isProcessingWorkout {
ProgressView()
.progressViewStyle(.circular)
.padding(.trailing, 10)
Text("Adding workout to health...")
.foregroundStyle(.accent)
} else {
Text("Add workout to health")
.foregroundStyle(.accent)
}
Spacer()
}
.padding(.vertical, 8)
}
.disabled(isProcessingWorkout)
} }
.disabled(isProcessingWorkout)
Section("Info") { Section("Info") {
DetailRow("Start", date: workout.startDate) DetailRow("Start", date: workout.startDate)
DetailRow("Duration", duration: workout.duration) DetailRow("Duration", duration: workout.duration)
@@ -113,10 +109,10 @@ struct WorkoutDetailView: View {
DetailRow(MetadataKeyName(key), value: "\(value)") DetailRow(MetadataKeyName(key), value: "\(value)")
} }
} label: { } label: {
DetailRow("Metadata", value: workout.metadata.count) DetailRow("Metadata", value: workout.metadata?.count ?? 0)
} }
DisclosureGroup { DisclosureGroup {
ForEach(privateMetadataFields, id:\.key) { (key, value) in ForEach(privateMetadata, id:\.key) { (key, value) in
DetailRow(MetadataKeyName(key), value: "\(value)") DetailRow(MetadataKeyName(key), value: "\(value)")
} }
} label: { } label: {
@@ -146,6 +142,9 @@ struct WorkoutDetailView: View {
Text("") Text("")
.frame(height: 150) .frame(height: 150)
.listRowBackground(WorkoutMapView(locations: locationSamples)) .listRowBackground(WorkoutMapView(locations: locationSamples))
.overlay(
NavigationLink(value: locationSamples) { }
.opacity(0))
} }
} }
} }
@@ -157,69 +156,174 @@ struct WorkoutDetailView: View {
.navigationDestination(for: HKWorkoutEvent.self) { event in .navigationDestination(for: HKWorkoutEvent.self) { event in
EventDetailView(event: event) EventDetailView(event: event)
} }
.navigationDestination(for: [CLLocation].self) { locations in
LocationSampleListView(samples: locations)
}
.onAppear(perform: loadSamples) .onAppear(perform: loadSamples)
.alert("Processing error", isPresented: $showErrorMessage) {
Button("Dismiss", role: .cancel) { }
} message: {
Text(errorMessage)
}
} }
private func addWorkoutToHealth() { private func show(_ error: String) {
guard let db = database.store else { DispatchQueue.main.async {
self.errorMessage = error
self.showErrorMessage = true
}
}
private func updateButtonText() {
if isProcessingWorkout {
if healthWorkout == nil {
self.healthButtonText = "Adding workout to Health..."
} else {
self.healthButtonText = "Deleting workout from Health..."
}
} else {
if healthWorkout == nil {
healthButtonText = "Add workout to Health"
} else {
healthButtonText = "Found matching workout in Health"
}
}
}
private func addOrDeleteHealthWorkout() {
guard !isProcessingWorkout else {
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.isProcessingWorkout = true self.isProcessingWorkout = true
updateButtonText()
} }
Task { Task {
do { if let healthWorkout {
try await insert(workout: workout, using: db) await delete(healthWorkout: healthWorkout)
} catch { } else {
print("Failed to insert workout: \(error)") await addWorkoutToHealth()
} }
DispatchQueue.main.async {
self.isProcessingWorkout = false
updateButtonText()
}
}
}
private func delete(healthWorkout: HKWorkout) async {
do {
try await store.store.delete(healthWorkout)
DispatchQueue.main.async {
self.healthWorkout = nil
self.isProcessingWorkout = false
}
} catch {
show("Failed to delete workout: \(error)")
DispatchQueue.main.async { DispatchQueue.main.async {
self.isProcessingWorkout = false self.isProcessingWorkout = false
} }
} }
} }
private func insert(workout: Workout, using db: HealthDatabase) async throws { private func addWorkoutToHealth() async {
try await store.requestAuthorization(toShare: HKWorkout.self, read: HKWorkout.self) guard let db = database.store else {
return
}
await insert(workout: workout, using: db)
}
private func requestAuthorization() async -> Bool {
do {
try await store.requestAuthorization(
toShare: HKWorkout.self, HKWorkoutRoute.self,
read: HKWorkout.self, HKWorkoutRoute.self)
return true
} catch {
show("Failed to check for workout permissions: \(error)")
return false
}
}
private func insert(workout: Workout, using db: HealthDatabase) async {
guard await requestAuthorization() else { return }
if store.authorizationStatus(for: HKWorkout.self) == .notDetermined || if store.authorizationStatus(for: HKWorkout.self) == .notDetermined ||
store.authorizationStatus(for: HKWorkoutRoute.self) == .notDetermined { store.authorizationStatus(for: HKWorkoutRoute.self) == .notDetermined {
print("Requesting workout sharing permission") print("Requesting workout permissions")
try await store.requestAuthorization(toShare: HKWorkout.self, HKWorkoutRoute.self, read: HKWorkout.self, HKWorkoutRoute.self) guard await requestAuthorization() else { return }
} }
guard store.authorizationStatus(for: HKWorkout.self) == .sharingAuthorized else { guard store.authorizationStatus(for: HKWorkout.self) == .sharingAuthorized else {
print("No sharing permission for workouts") show("No sharing permission for workouts")
return return
} }
guard store.authorizationStatus(for: HKWorkoutRoute.self) == .sharingAuthorized else { guard store.authorizationStatus(for: HKWorkoutRoute.self) == .sharingAuthorized else {
print("No sharing permission for workout routes") show("No sharing permission for workout routes")
return return
} }
var samples: [HKSample]
do { do {
print("Getting samples") samples = try db.store.samples(associatedWith: workout)
let samples = try db.store.samples(associatedWith: workout) } catch {
let route = try db.store.route(associatedWith: workout) show("Failed to access samples associated with workout: \(error)")
.map { try db.store.locations(associatedWith: $0) } ?? [] return
}
print("Saving workout in Health: \(samples.count) samples, \(route.count) locations") // Add missing samples from statistics
let newSamples = makeStatisticSamples(workout, in: db)
for sample in newSamples {
guard !samples.contains(where: { $0.sampleType == sample.sampleType }) else {
continue
}
let typeName = HKQuantityTypeIdentifier(rawValue: sample.quantityType.identifier).description
print("Adding missing sample \(typeName)")
samples.append(sample)
}
let savedWorkout = try await workout.insert( let route: WorkoutRoute?
do {
route = try db.route(associatedWith: workout)
} catch {
show("Failed to get route associated with workout: \(error)")
return
}
let locations: [CLLocation]
do {
locations = try route.map {
try db.locations(associatedWith: $0)
} ?? []
} catch {
show("Failed to get locations associated with route: \(error)")
return
}
print("Saving workout in Health: \(samples.count) samples, \(locations.count) locations")
let savedWorkout: HKWorkout
do {
savedWorkout = try await workout.insert(
into: store.store, into: store.store,
samples: samples, samples: samples,
route: route) route: locations)
DispatchQueue.main.async { } catch {
self.healthWorkout = savedWorkout show("Failed to insert workout: \(error)")
} return
print("Saved workout in Health") }
let energySamples: [ActiveEnergyBurned] = try await store.samples(associatedWith: savedWorkout) DispatchQueue.main.async {
print("Found \(energySamples.count) energy samples") self.healthWorkout = savedWorkout
if let route = try await store.route(associatedWith: savedWorkout) { }
let locations = try await store.locations(associatedWith: route) }
print("Found \(locations.count)/\(locationSamples.count) locations associated with saved workout")
} else { private func makeStatisticSamples(_ workout: Workout, in db: HealthDatabase) -> [HKQuantitySample] {
print("No route associated with saved workout") let startDate = workout.startDate
let endDate = workout.endDate
let activity = workout.workoutActivities[0]
do {
let statistics = try db.statistics(associatedWith: activity)
return statistics.map {
.init(type: $0, quantity: $1.average, start: startDate, end: endDate)
} }
} catch { } catch {
print("Failed to add workout to health: \(error)") print("Failed to get statistics for activity: \(error)")
return []
} }
} }
@@ -243,9 +347,8 @@ struct WorkoutDetailView: View {
self.heartRateSamples = samples self.heartRateSamples = samples
self.samples = graphSamples self.samples = graphSamples
} }
print("Loaded \(samples.count) heart rate samples from database")
} catch { } catch {
print("Failed to load heart rate samples from database: \(error)") show("Failed to load heart rate samples from database: \(error)")
} }
} }
@@ -261,7 +364,7 @@ struct WorkoutDetailView: View {
} }
print("Loaded \(locations.count) locations from database") print("Loaded \(locations.count) locations from database")
} catch { } catch {
print("Failed to load locations or route from database: \(error)") show("Failed to load locations or route from database: \(error)")
} }
} }
@@ -270,11 +373,10 @@ struct WorkoutDetailView: View {
let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true) let metadata = try db.store.metadata(for: workout.uuid, includePrivateMetadata: true)
.filter { $0.key.hasPrefix("_HKPrivate") } .filter { $0.key.hasPrefix("_HKPrivate") }
DispatchQueue.main.async { DispatchQueue.main.async {
self.privateMetadata = metadata self.privateMetadata = metadata.sorted { $0.key }
} }
print("Loaded \(metadata.count) private metadata fields")
} catch { } catch {
print("Failed to load private metadata from database: \(error)") show("Failed to load private metadata from database: \(error)")
} }
} }
@@ -282,10 +384,18 @@ struct WorkoutDetailView: View {
guard HKHealthStore.isHealthDataAvailable() else { guard HKHealthStore.isHealthDataAvailable() else {
return return
} }
DispatchQueue.main.async {
self.healthButtonText = "Checking for matching workout..."
self.isProcessingWorkout = true
}
do { do {
try await checkPermissionsAndFindWorkout() try await checkPermissionsAndFindWorkout()
} catch { } catch {
print("Failed to search for workout: \(error)") show("Failed to search for similar workout in Health: \(error)")
}
DispatchQueue.main.async {
self.isProcessingWorkout = false
updateButtonText()
} }
} }
@@ -298,7 +408,7 @@ struct WorkoutDetailView: View {
await findWorkoutInHealth() await findWorkoutInHealth()
return return
@unknown default: @unknown default:
print("Unknown permission for workouts") show("Unknown permission for workouts")
return return
} }
} }
@@ -309,7 +419,7 @@ struct WorkoutDetailView: View {
private func findWorkoutInHealth() async { private func findWorkoutInHealth() async {
guard let activityType = workout.workoutActivities.first?.workoutConfiguration.activityType else { guard let activityType = workout.workoutActivities.first?.workoutConfiguration.activityType else {
print("No activity type to find workout") show("No activity type associated with workout")
return return
} }
@@ -322,12 +432,11 @@ struct WorkoutDetailView: View {
return return
} }
print("Found matching workout in health")
DispatchQueue.main.async { DispatchQueue.main.async {
self.healthWorkout = workout self.healthWorkout = workout
} }
} catch { } catch {
print("Failed to search for matching workout: \(error)") show("Failed to search for matching workout: \(error)")
} }
} }
} }
@@ -335,12 +444,12 @@ struct WorkoutDetailView: View {
#Preview { #Preview {
return NavigationStack { return NavigationStack {
WorkoutDetailView(workout: .mock1) WorkoutDetailView(workout: .mock1)
.environmentObject(Database.mock) .environmentObject(Database.empty)
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
} }
} }
extension String: Identifiable { extension String: @retroactive Identifiable {
public var id: Self { self } public var id: Self { self }
} }

View File

@@ -9,7 +9,7 @@ struct WorkoutListRow: View {
let workout: Workout let workout: Workout
var indoor: Bool { var indoor: Bool {
guard let isIndoor: Bool = workout.metadata[.indoorWorkout] else { guard let isIndoor: Bool = workout.metadata?.indoorWorkout else {
return false return false
} }
return isIndoor return isIndoor
@@ -17,7 +17,7 @@ struct WorkoutListRow: View {
var type: HKWorkoutActivityType { var type: HKWorkoutActivityType {
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
if let type: HKWorkoutActivityType = workout.metadata.activityType { if let type: HKWorkoutActivityType = workout.metadata?.activityType {
return type return type
} }
} }

43
README.md Normal file
View File

@@ -0,0 +1,43 @@
![](screens.png)
# HealthImport
This iOS app is a prototype to open the raw databases containing Apple Health data. These `healthdb_secure.sqlite` files can be extracted from (encrypted ) iOS Backups.
The structure of the database has been reverse-engineered so that most health data can be viewed and extracted.
This app is a front-end for [HealthDB](https://github.com/christophhagen/healthdb), which provides the interface to access the database files.
## Features
HealthImport offers a view basic features to view and extract health data, mostly centered around workouts.
### Managing databases
Multiple databases can be imported into the app.
The databases can be renamed and a default database can be selected.
One database can be opened at a time, so that the contents can be viewed.
### Workouts
The existing workouts contained in each database can be viewed in the "Workouts" tab.
The interface has been designed to be similar to the Apple Fitness app.
A filter allows selecting only those workouts of a specific activity type, and the details of a workout can be viewed.
It shows the workout route, heart rate, and statistics for each workout.
If the app has the required permissions then it also checks if a similar workout already exists on the phone, and can add missing workouts from the database to `Health`.
This feature is useful to restore workouts that have been deleted from an existing backup.
### Samples
The tab "Health" is structured similar to the Apple Health app, and allows to view different samples stored in the database.
Each supported type can be searched by start and end date, and shows a simple list of the samples found.
## Roadmap
In the future, additional features are envisioned:
- View most recent samples, similar to the Apple Health app
- Support viewing special sample types, like ECGs
- Show details for samples, and allow adding them to Apple Health
- Support more filtering options to find samples

BIN
screens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB