Compare commits
No commits in common. "03469446e19ce7843b2b98c98f5df3027cf0725f" and "c8ab7909ee19b7a50301ad8a792e5a7769d255b5" have entirely different histories.
03469446e1
...
c8ab7909ee
BIN
HealthImport.jpg
BIN
HealthImport.jpg
Binary file not shown.
Before Width: | Height: | Size: 461 KiB |
BIN
HealthImport.key
BIN
HealthImport.key
Binary file not shown.
@ -11,6 +11,7 @@
|
|||||||
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 */; };
|
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 */; };
|
||||||
@ -24,11 +25,14 @@
|
|||||||
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 */; };
|
||||||
@ -36,26 +40,8 @@
|
|||||||
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 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528B2BA21C0700BF5E9B /* Database.swift */; };
|
E2E5528C2BA21C0700BF5E9B /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528B2BA21C0700BF5E9B /* HealthDatabase.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 */; };
|
||||||
@ -73,7 +59,6 @@
|
|||||||
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 */
|
||||||
@ -84,6 +69,7 @@
|
|||||||
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>"; };
|
||||||
@ -93,41 +79,25 @@
|
|||||||
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>"; };
|
|
||||||
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 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
E2E5528B2BA21C0700BF5E9B /* HealthDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthDatabase.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.xml; path = Info.plist; sourceTree = "<group>"; };
|
E2E552932BA23B8F00BF5E9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; 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>"; };
|
||||||
@ -141,7 +111,6 @@
|
|||||||
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 */
|
||||||
@ -185,17 +154,26 @@
|
|||||||
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 */,
|
||||||
885002602B5C273E00E7D4DB /* Preview Content */,
|
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */,
|
||||||
E2E552BE2BAB38AC00BF5E9B /* Samples */,
|
8850026A2B5C276B00E7D4DB /* Resources */,
|
||||||
E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */,
|
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
||||||
E2E552872BA2193B00BF5E9B /* Tabs */,
|
E2E552872BA2193B00BF5E9B /* Tabs */,
|
||||||
E2E552BC2BAAE9A900BF5E9B /* Workouts */,
|
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 */,
|
||||||
|
E20881D42B76944A00D41D95 /* Test.swift */,
|
||||||
|
885002832B5C37C600E7D4DB /* Support */,
|
||||||
);
|
);
|
||||||
path = HealthImport;
|
path = HealthImport;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -203,7 +181,6 @@
|
|||||||
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 */,
|
||||||
@ -215,7 +192,15 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
885002832B5C37C600E7D4DB /* Extensions */ = {
|
8850026A2B5C276B00E7D4DB /* Resources */ = {
|
||||||
|
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 */,
|
||||||
@ -233,26 +218,7 @@
|
|||||||
E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */,
|
E2E552B62BA9A69400BF5E9B /* Color+Extensions.swift */,
|
||||||
E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */,
|
E2E552B82BA9A77D00BF5E9B /* HKWorkoutActivityType+Icon.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Support;
|
||||||
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 */ = {
|
||||||
@ -268,7 +234,7 @@
|
|||||||
E2E5528A2BA21BFB00BF5E9B /* Model */ = {
|
E2E5528A2BA21BFB00BF5E9B /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2E5528B2BA21C0700BF5E9B /* Database.swift */,
|
E2E5528B2BA21C0700BF5E9B /* HealthDatabase.swift */,
|
||||||
E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */,
|
E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */,
|
||||||
E2E552912BA236D000BF5E9B /* DatabaseFile.swift */,
|
E2E552912BA236D000BF5E9B /* DatabaseFile.swift */,
|
||||||
);
|
);
|
||||||
@ -282,38 +248,17 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
E2E552BC2BAAE9A900BF5E9B /* Workouts */ = {
|
E2E5529F2BA4B13100BF5E9B /* UI Elements */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
|
||||||
885002942B5D147100E7D4DB /* DetailRow.swift */,
|
|
||||||
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
|
|
||||||
E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.swift */,
|
|
||||||
E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */,
|
E2E552A02BA4B14600BF5E9B /* HeartRateSample.swift */,
|
||||||
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */,
|
E2E552A22BA4B58F00BF5E9B /* HeartRateGraph.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 = Workouts;
|
path = "UI Elements";
|
||||||
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 */
|
||||||
@ -392,6 +337,7 @@
|
|||||||
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 */,
|
||||||
);
|
);
|
||||||
@ -404,62 +350,46 @@
|
|||||||
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 /* Database.swift in Sources */,
|
E2E5528C2BA21C0700BF5E9B /* HealthDatabase.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 */,
|
||||||
@ -723,7 +653,7 @@
|
|||||||
repositoryURL = "https://github.com/christophhagen/HealthDB";
|
repositoryURL = "https://github.com/christophhagen/HealthDB";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 0.4.0;
|
minimumVersion = 0.3.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
|
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/christophhagen/HealthDB",
|
"location" : "https://github.com/christophhagen/HealthDB",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "cd077d034d88d31cf283bcf5c0663cb027496a66",
|
"revision" : "6dfcafc66d59bc5887f7bfd66818a169cd7b73dd",
|
||||||
"version" : "0.4.1"
|
"version" : "0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
97
HealthImport/ActivityDetailView.swift
Normal file
97
HealthImport/ActivityDetailView.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
71
HealthImport/ActivitySamplesView.swift
Normal file
71
HealthImport/ActivitySamplesView.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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)
|
||||||
|
}
|
@ -32,6 +32,7 @@ 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 {
|
||||||
@ -40,6 +41,7 @@ struct HealthImportApp: App {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
print("Setting selection to workouts")
|
||||||
self.selection = .workouts
|
self.selection = .workouts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,32 +3,20 @@ import SQLite
|
|||||||
import HealthKit
|
import HealthKit
|
||||||
import HealthDB
|
import HealthDB
|
||||||
|
|
||||||
extension HealthDatabase {
|
extension Database {
|
||||||
|
|
||||||
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: HealthDatabase {
|
static var mock: Database {
|
||||||
let bundleUrl = databaseFileUrl!
|
let bundleUrl = Database.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)
|
||||||
}
|
}
|
||||||
return try! HealthDatabase(fileUrl: local)
|
let store = 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 {
|
||||||
|
@ -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(value: UInt64(1)),
|
"_HKPrivateWorkoutSegmentEventSubtype": NSNumber(1),
|
||||||
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
|
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
|
||||||
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(1)),
|
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(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": NSNumber(value: UInt64(0)),
|
"_HKPrivateMetadataIsPartialSplit": 0,
|
||||||
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
|
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
|
||||||
"_HKPrivateWorkoutSegmentEventSubtype": NSNumber(value: UInt64(1)),
|
"_HKPrivateWorkoutSegmentEventSubtype": 1,
|
||||||
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(2))
|
"_HKPrivateMetadataSplitMeasuringSystem": 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": NSNumber(value: UInt64(0)),
|
"_HKPrivateMetadataIsPartialSplit": 0,
|
||||||
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
|
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
|
||||||
"_HKPrivateWorkoutSegmentEventSubtype": NSNumber(value: UInt64(1)),
|
"_HKPrivateWorkoutSegmentEventSubtype": 1,
|
||||||
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(value: UInt64(2)),
|
"_HKPrivateMetadataSplitMeasuringSystem": 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),
|
||||||
|
30
HealthImport/SampleListView.swift
Normal file
30
HealthImport/SampleListView.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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
|
|
||||||
[] })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -8,73 +8,21 @@ struct SamplesTab: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
if let db = database.store {
|
Label("Activity", systemSymbol: .flame)
|
||||||
NavigationLink {
|
Label("Body Measurements", systemSymbol: .figure)
|
||||||
ActivitySamplesList(database: db)
|
Label("Cycle Tracking", systemSymbol: .circleHexagonpath)
|
||||||
} label: {
|
Label("Hearing", systemSymbol: .ear)
|
||||||
Label("Activity", systemSymbol: .flame)
|
Label("Heart", systemSymbol: .heartFill)
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
@ -82,6 +30,6 @@ struct SamplesTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SamplesTab(database: Database.empty)
|
SamplesTab(database: Database())
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
@ -146,5 +146,26 @@ struct WorkoutTab: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
WorkoutTab()
|
WorkoutTab()
|
||||||
.environmentObject(Database.empty)
|
.environmentObject(Database.mock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
*/
|
||||||
|
227
HealthImport/Test.swift
Normal file
227
HealthImport/Test.swift
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
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.")
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,24 +29,19 @@ struct WorkoutDetailView: View {
|
|||||||
private var locationSamples: [CLLocation] = []
|
private var locationSamples: [CLLocation] = []
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var privateMetadata: [(key: String, value: Any)] = []
|
private var privateMetadata: [String : Any] = [:]
|
||||||
|
|
||||||
@State
|
private var metadataFields: [(key: String, value: Any)] {
|
||||||
private var showErrorMessage = false
|
workout.metadata.sorted { $0.key }
|
||||||
|
}
|
||||||
|
|
||||||
@State
|
private var privateMetadataFields: [(key: String, value: Any)] {
|
||||||
private var errorMessage: String = ""
|
privateMetadata.sorted { $0.key }
|
||||||
|
}
|
||||||
|
|
||||||
@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
|
||||||
@ -54,28 +49,37 @@ struct WorkoutDetailView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Button(action: addOrDeleteHealthWorkout) {
|
if healthWorkout != nil {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
if isProcessingWorkout {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(.circular)
|
|
||||||
.padding(.trailing, 10)
|
|
||||||
}
|
|
||||||
VStack {
|
VStack {
|
||||||
Text(healthButtonText)
|
Text("Matching workout found in Health")
|
||||||
.foregroundStyle(.accent)
|
.foregroundStyle(.black)
|
||||||
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)
|
||||||
@ -109,10 +113,10 @@ struct WorkoutDetailView: View {
|
|||||||
DetailRow(MetadataKeyName(key), value: "\(value)")
|
DetailRow(MetadataKeyName(key), value: "\(value)")
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
DetailRow("Metadata", value: workout.metadata?.count ?? 0)
|
DetailRow("Metadata", value: workout.metadata.count)
|
||||||
}
|
}
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
ForEach(privateMetadata, id:\.key) { (key, value) in
|
ForEach(privateMetadataFields, id:\.key) { (key, value) in
|
||||||
DetailRow(MetadataKeyName(key), value: "\(value)")
|
DetailRow(MetadataKeyName(key), value: "\(value)")
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
@ -142,9 +146,6 @@ 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,174 +157,69 @@ 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 show(_ error: String) {
|
private func addWorkoutToHealth() {
|
||||||
DispatchQueue.main.async {
|
guard let db = database.store else {
|
||||||
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 {
|
||||||
if let healthWorkout {
|
do {
|
||||||
await delete(healthWorkout: healthWorkout)
|
try await insert(workout: workout, using: db)
|
||||||
} else {
|
} catch {
|
||||||
await addWorkoutToHealth()
|
print("Failed to insert workout: \(error)")
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isProcessingWorkout = false
|
self.isProcessingWorkout = false
|
||||||
updateButtonText()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func delete(healthWorkout: HKWorkout) async {
|
private func insert(workout: Workout, using db: HealthDatabase) async throws {
|
||||||
do {
|
try await store.requestAuthorization(toShare: HKWorkout.self, read: HKWorkout.self)
|
||||||
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 {
|
|
||||||
self.isProcessingWorkout = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addWorkoutToHealth() async {
|
|
||||||
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 permissions")
|
print("Requesting workout sharing permission")
|
||||||
guard await requestAuthorization() else { return }
|
try await store.requestAuthorization(toShare: HKWorkout.self, HKWorkoutRoute.self, read: HKWorkout.self, HKWorkoutRoute.self)
|
||||||
}
|
}
|
||||||
guard store.authorizationStatus(for: HKWorkout.self) == .sharingAuthorized else {
|
guard store.authorizationStatus(for: HKWorkout.self) == .sharingAuthorized else {
|
||||||
show("No sharing permission for workouts")
|
print("No sharing permission for workouts")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard store.authorizationStatus(for: HKWorkoutRoute.self) == .sharingAuthorized else {
|
guard store.authorizationStatus(for: HKWorkoutRoute.self) == .sharingAuthorized else {
|
||||||
show("No sharing permission for workout routes")
|
print("No sharing permission for workout routes")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var samples: [HKSample]
|
|
||||||
do {
|
do {
|
||||||
samples = try db.store.samples(associatedWith: workout)
|
print("Getting samples")
|
||||||
} catch {
|
let samples = try db.store.samples(associatedWith: workout)
|
||||||
show("Failed to access samples associated with workout: \(error)")
|
let route = try db.store.route(associatedWith: workout)
|
||||||
return
|
.map { try db.store.locations(associatedWith: $0) } ?? []
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing samples from statistics
|
print("Saving workout in Health: \(samples.count) samples, \(route.count) locations")
|
||||||
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 route: WorkoutRoute?
|
let savedWorkout = try await workout.insert(
|
||||||
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: locations)
|
route: route)
|
||||||
} catch {
|
DispatchQueue.main.async {
|
||||||
show("Failed to insert workout: \(error)")
|
self.healthWorkout = savedWorkout
|
||||||
return
|
}
|
||||||
}
|
print("Saved workout in Health")
|
||||||
DispatchQueue.main.async {
|
let energySamples: [ActiveEnergyBurned] = try await store.samples(associatedWith: savedWorkout)
|
||||||
self.healthWorkout = savedWorkout
|
print("Found \(energySamples.count) energy samples")
|
||||||
}
|
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")
|
||||||
private func makeStatisticSamples(_ workout: Workout, in db: HealthDatabase) -> [HKQuantitySample] {
|
} else {
|
||||||
let startDate = workout.startDate
|
print("No route associated with saved workout")
|
||||||
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 get statistics for activity: \(error)")
|
print("Failed to add workout to health: \(error)")
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,8 +243,9 @@ 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 {
|
||||||
show("Failed to load heart rate samples from database: \(error)")
|
print("Failed to load heart rate samples from database: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +261,7 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
print("Loaded \(locations.count) locations from database")
|
print("Loaded \(locations.count) locations from database")
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to load locations or route from database: \(error)")
|
print("Failed to load locations or route from database: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,10 +270,11 @@ 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.sorted { $0.key }
|
self.privateMetadata = metadata
|
||||||
}
|
}
|
||||||
|
print("Loaded \(metadata.count) private metadata fields")
|
||||||
} catch {
|
} catch {
|
||||||
show("Failed to load private metadata from database: \(error)")
|
print("Failed to load private metadata from database: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,18 +282,10 @@ 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 {
|
||||||
show("Failed to search for similar workout in Health: \(error)")
|
print("Failed to search for workout: \(error)")
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isProcessingWorkout = false
|
|
||||||
updateButtonText()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +298,7 @@ struct WorkoutDetailView: View {
|
|||||||
await findWorkoutInHealth()
|
await findWorkoutInHealth()
|
||||||
return
|
return
|
||||||
@unknown default:
|
@unknown default:
|
||||||
show("Unknown permission for workouts")
|
print("Unknown permission for workouts")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,7 +309,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 {
|
||||||
show("No activity type associated with workout")
|
print("No activity type to find workout")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,11 +322,12 @@ 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 {
|
||||||
show("Failed to search for matching workout: \(error)")
|
print("Failed to search for matching workout: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -444,7 +335,7 @@ struct WorkoutDetailView: View {
|
|||||||
#Preview {
|
#Preview {
|
||||||
return NavigationStack {
|
return NavigationStack {
|
||||||
WorkoutDetailView(workout: .mock1)
|
WorkoutDetailView(workout: .mock1)
|
||||||
.environmentObject(Database.empty)
|
.environmentObject(Database.mock)
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,125 +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
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user