Use new database framework

This commit is contained in:
Christoph Hagen 2024-03-08 17:42:55 +01:00
parent 656cbaaf10
commit d99d83a085
46 changed files with 303 additions and 2968 deletions

View File

@ -12,60 +12,35 @@
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; };
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */; };
8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */; };
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002702B5C299900E7D4DB /* HealthDatabase.swift */; };
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; };
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; };
8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */; };
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027E2B5C36A700E7D4DB /* Workout.swift */; };
885002852B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */; };
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */; };
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */; };
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */; };
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */; };
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002922B5D129300E7D4DB /* ActivityDetailView.swift */; };
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002942B5D147100E7D4DB /* DetailRow.swift */; };
885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */; };
885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */; };
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */; };
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029C2B5D197300E7D4DB /* EventDetailView.swift */; };
8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */; };
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002A22B5D217600E7D4DB /* MetadataValue.swift */; };
885002A62B5D296700E7D4DB /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A52B5D296700E7D4DB /* Collections */; };
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A72B5D296700E7D4DB /* DequeModule */; };
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A92B5D296700E7D4DB /* OrderedCollections */; };
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */; };
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC742B626B19005B83D3 /* Metadata+Mock.swift */; };
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; };
E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKeysTable.swift */; };
E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */; };
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* SamplesTable.swift */; };
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; };
E201EC812B631708005B83D3 /* Goal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC802B631708005B83D3 /* Goal.swift */; };
E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */; };
E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = E20881D22B76912000D41D95 /* HealthKitExtensions */; };
E20881D52B76944A00D41D95 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20881D42B76944A00D41D95 /* Test.swift */; };
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; };
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; };
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */; };
E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6832B5E76A4003A8873 /* Location+Mock.swift */; };
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6852B5FBF0B003A8873 /* Sample.swift */; };
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */; };
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */; };
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; };
E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */; };
E27BC6902B5FCEA4003A8873 /* WorkoutActivitiesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */; };
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */; };
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6932B5FD587003A8873 /* Workout+Mock.swift */; };
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */; };
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; };
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */; };
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */; };
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */; };
E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA02B99FFDD00BAD02E /* HKDatabase */; };
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; };
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */; };
E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */; };
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */; };
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */; };
E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -75,55 +50,29 @@
8850025E2B5C273E00E7D4DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<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>"; };
885002702B5C299900E7D4DB /* HealthDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthDatabase.swift; sourceTree = "<group>"; };
885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutsTable.swift; sourceTree = "<group>"; };
8850027E2B5C36A700E7D4DB /* Workout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workout.swift; sourceTree = "<group>"; };
885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEvent+Identifiable.swift"; sourceTree = "<group>"; };
885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivityType+Extensions.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>"; };
885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEventType+Extensions.swift"; sourceTree = "<group>"; };
885002922B5D129300E7D4DB /* ActivityDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityDetailView.swift; sourceTree = "<group>"; };
885002942B5D147100E7D4DB /* DetailRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRow.swift; sourceTree = "<group>"; };
885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSessionLocationType+Extensions.swift"; sourceTree = "<group>"; };
885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSwimmingLocationType+Extensions.swift"; sourceTree = "<group>"; };
8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = "<group>"; };
8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValuesTable.swift; sourceTree = "<group>"; };
885002A22B5D217600E7D4DB /* MetadataValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValue.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>"; };
E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = "<group>"; };
E201EC782B627572005B83D3 /* MetadataKeysTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKeysTable.swift; sourceTree = "<group>"; };
E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataTables.swift; sourceTree = "<group>"; };
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTable.swift; sourceTree = "<group>"; };
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = "<group>"; };
E201EC802B631708005B83D3 /* Goal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Goal.swift; sourceTree = "<group>"; };
E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSeriesDataTable.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>"; };
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>"; };
E27BC6832B5E76A4003A8873 /* Location+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+Mock.swift"; sourceTree = "<group>"; };
E27BC6852B5FBF0B003A8873 /* Sample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sample.swift; sourceTree = "<group>"; };
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySamplesTable.swift; sourceTree = "<group>"; };
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStringsTable.swift; sourceTree = "<group>"; };
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesView.swift; sourceTree = "<group>"; };
E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEventsTable.swift; sourceTree = "<group>"; };
E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivitiesTable.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>"; };
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>"; };
E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKHealthStoreInterface.swift; sourceTree = "<group>"; };
E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKHealthStore+Interface.swift"; sourceTree = "<group>"; };
E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKDatabaseFile+Interface.swift"; sourceTree = "<group>"; };
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMetadata.pb.swift; sourceTree = "<group>"; };
E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectsTable.swift; sourceTree = "<group>"; };
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvenancesTable.swift; sourceTree = "<group>"; };
E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = "<group>"; };
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivity+Comparable.swift"; sourceTree = "<group>"; };
E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSeriesTable.swift; sourceTree = "<group>"; };
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -132,6 +81,8 @@
buildActionMask = 2147483647;
files = (
885002A62B5D296700E7D4DB /* Collections in Frameworks */,
E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */,
E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */,
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */,
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */,
@ -161,6 +112,7 @@
885002592B5C273C00E7D4DB /* HealthImport */ = {
isa = PBXGroup;
children = (
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */,
8850026A2B5C276B00E7D4DB /* Resources */,
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
@ -174,9 +126,7 @@
885002942B5D147100E7D4DB /* DetailRow.swift */,
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
885002602B5C273E00E7D4DB /* Preview Content */,
885002702B5C299900E7D4DB /* HealthDatabase.swift */,
E2FDFF1D2B6BD1F00080A7B3 /* API */,
885002812B5C37B700E7D4DB /* Model */,
E20881D42B76944A00D41D95 /* Test.swift */,
885002832B5C37C600E7D4DB /* Support */,
);
path = HealthImport;
@ -204,69 +154,20 @@
path = Resources;
sourceTree = "<group>";
};
885002812B5C37B700E7D4DB /* Model */ = {
isa = PBXGroup;
children = (
E2FDFF232B6C509D0080A7B3 /* Tables */,
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */,
E201EC802B631708005B83D3 /* Goal.swift */,
E201EC762B626FC1005B83D3 /* MetadataKey.swift */,
885002A22B5D217600E7D4DB /* MetadataValue.swift */,
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
8850027E2B5C36A700E7D4DB /* Workout.swift */,
E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */,
885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */,
);
path = Model;
sourceTree = "<group>";
};
885002832B5C37C600E7D4DB /* Support */ = {
isa = PBXGroup;
children = (
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */,
8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */,
885002782B5C320400E7D4DB /* Optional+Extensions.swift */,
885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */,
885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */,
885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */,
885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */,
8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */,
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */,
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */,
E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */,
);
path = Support;
sourceTree = "<group>";
};
E2FDFF1D2B6BD1F00080A7B3 /* API */ = {
isa = PBXGroup;
children = (
E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */,
E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */,
E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */,
);
path = API;
sourceTree = "<group>";
};
E2FDFF232B6C509D0080A7B3 /* Tables */ = {
isa = PBXGroup;
children = (
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */,
E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */,
E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */,
E201EC782B627572005B83D3 /* MetadataKeysTable.swift */,
E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */,
8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */,
E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */,
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */,
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */,
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */,
E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */,
E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */,
8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */,
);
path = Tables;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -289,6 +190,8 @@
885002A72B5D296700E7D4DB /* DequeModule */,
885002A92B5D296700E7D4DB /* OrderedCollections */,
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */,
E20881D22B76912000D41D95 /* HealthKitExtensions */,
E2A38EA02B99FFDD00BAD02E /* HKDatabase */,
);
productName = HealthImport;
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
@ -322,6 +225,8 @@
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */,
885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */,
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */,
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */,
);
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
projectDirPath = "";
@ -351,55 +256,28 @@
buildActionMask = 2147483647;
files = (
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */,
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */,
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */,
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */,
885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */,
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */,
E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */,
E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */,
885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */,
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */,
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */,
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
885002852B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift in Sources */,
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */,
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */,
E27BC6902B5FCEA4003A8873 /* WorkoutActivitiesTable.swift in Sources */,
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */,
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */,
E2FDFF222B6BE35B0080A7B3 /* EventMetadata.pb.swift in Sources */,
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */,
E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */,
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */,
E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */,
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */,
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */,
8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */,
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */,
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */,
E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */,
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */,
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */,
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */,
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
E20881D52B76944A00D41D95 /* Test.swift in Sources */,
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */,
E201EC812B631708005B83D3 /* Goal.swift in Sources */,
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */,
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -459,7 +337,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -516,7 +394,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
@ -531,12 +409,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = HealthImport/HealthImport.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"HealthImport/Preview Content\"";
DEVELOPMENT_TEAM = H8WR4M6QQ4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Manage all the health data your choose.";
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Manage all the health data your choose.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -560,12 +441,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = HealthImport/HealthImport.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"HealthImport/Preview Content\"";
DEVELOPMENT_TEAM = H8WR4M6QQ4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Manage all the health data your choose.";
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Manage all the health data your choose.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -624,6 +508,22 @@
minimumVersion = 1.0.6;
};
};
E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/christophhagen/HealthKitExtensions";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.3.0;
};
};
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/christophhagen/iOSHealthDBInterface";
requirement = {
branch = main;
kind = branch;
};
};
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-protobuf.git";
@ -655,6 +555,16 @@
package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = OrderedCollections;
};
E20881D22B76912000D41D95 /* HealthKitExtensions */ = {
isa = XCSwiftPackageProductDependency;
package = E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */;
productName = HealthKitExtensions;
};
E2A38EA02B99FFDD00BAD02E /* HKDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */;
productName = HKDatabase;
};
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = {
isa = XCSwiftPackageProductDependency;
package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */;

View File

@ -1,5 +1,24 @@
{
"originHash" : "d0067804897f78a2b51cdf96069649aae9f635cdf94333101762cb0c84fd39ae",
"pins" : [
{
"identity" : "healthkitextensions",
"kind" : "remoteSourceControl",
"location" : "https://github.com/christophhagen/HealthKitExtensions",
"state" : {
"revision" : "88625ad3480ceea974e47f61e1abb05a84896c73",
"version" : "0.3.0"
}
},
{
"identity" : "ioshealthdbinterface",
"kind" : "remoteSourceControl",
"location" : "https://github.com/christophhagen/iOSHealthDBInterface",
"state" : {
"branch" : "main",
"revision" : "d638c367f3bbdbffefdf0a4dde8fc0afd73bbaad"
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
@ -28,5 +47,5 @@
}
}
],
"version" : 2
"version" : 3
}

View File

@ -1,68 +0,0 @@
import Foundation
import HealthKit
struct HKNotSupportedError: Error {
}
extension HealthDatabase {
public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery {
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
switch sampleType {
case is HKWorkoutType:
workouts(predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
return query
case is HKCorrelationType:
// TODO: Implement
break
case is HKQuantityType:
// TODO: Implement
break
case is HKAudiogramSampleType:
// TODO: Implement
break
case is HKElectrocardiogramType:
// TODO: Implement
break
case is HKPrescriptionType:
// TODO: Implement
break
case is HKClinicalType:
// TODO: Implement
break
case is HKCategoryType:
// TODO: Implement
break
case is HKDocumentType:
// TODO: Implement
break
case is HKSeriesType:
// TODO: Implement
break
//case is HKCharacteristicType:
// break
//case is HKActivitySummaryType:
// break
default:
break
}
resultsHandler(query, nil, HKNotSupportedError())
return query
//sampleType.isMinimumDurationRestricted
//sampleType.isMaximumDurationRestricted
//sampleType.maximumAllowedDuration
//sampleType.allowsRecalibrationForEstimates
}
private func workouts(predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) {
#warning("Get workouts and filter them")
// Example predicates
// HKQuery.predicateForWorkouts(with: .greaterThanOrEqualTo, duration: 1)
// HKQuery.predicateForWorkouts(with: HKWorkoutActivityType)
}
}

View File

@ -1,14 +0,0 @@
import Foundation
import HealthKit
extension HKHealthStore: HKHealthStoreInterface {
/**
Creates a sample query and executes it.
*/
public func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery {
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
execute(query)
return query
}
}

View File

@ -1,206 +0,0 @@
import Foundation
import HealthKit
import UIKit
public protocol HKHealthStoreInterface {
// MARK: - Accessing HealthKit
/**
Returns the apps authorization status for sharing the specified data type.
This method checks the authorization status for saving data.
To help prevent possible leaks of sensitive health information, your app cannot determine whether or not a user has granted permission to read data.
If you are not given permission, it simply appears as if there is no data of the requested type in the HealthKit store.
If your app is given share permission but not read permission, you see only the data that your app has written to the store.
Data from other sources remains hidden.
- Parameter type: The type of data. This can be any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType`` , ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType`` or ``HKCorrelationType`` classes).
- Returns: A value indicating the apps authorization status for this type. For a list of possible values, see `HKAuthorizationStatus`.
*/
func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus
/**
Indicates whether the system presents the user with a permission sheet if your app requests authorization for the provided types.
When working with clinical types, users may need to reauthorize access when new data is added.
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func statusForAuthorizationRequest(toShare typesToShare: Set<HKSampleType>, read typesToRead: Set<HKObjectType>) async throws -> HKAuthorizationRequestStatus
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
*/
func getRequestStatusForAuthorization(
toShare typesToShare: Set<HKSampleType>,
read typesToRead: Set<HKObjectType>,
completion: @escaping (HKAuthorizationRequestStatus, Error?) -> Void
)
/**
Returns a Boolean value that indicates whether HealthKit is available on this device.
By default, HealthKit data is available on iOS and watchOS. HealthKit data is also available on iPadOS 17 or later. However, devices running in an enterprise environment may restrict access to HealthKit data.
Additionally, while the HealthKit framework is available on iPadOS 16 and earlier and on MacOS 13 and later, these devices dont have a copy of the HealthKit store. This means you can include HealthKit code in apps running on these devices, simplifying the creation of multiplatform apps. However, they cant read or write HealthKit data, and calls to ``isHealthDataAvailable()`` return ``false``.
- Returns: `true` if HealthKit is available; otherwise, `false`.
*/
static func isHealthDataAvailable() -> Bool
/**
Returns a Boolean value that indicates whether the current device supports clinical records.
This method returns true if the device is set to a locale where clinical records are supported, or if the user already has clinical records downloaded to their HealthKit store. Otherwise, it returns false.
This method lets users switch their locale without losing their health records.
*/
func supportsHealthRecords() -> Bool
/**
Requests permission to save and read the specified data types.
HealthKit performs these requests asynchronously. If you call this method with a new data type (a type of data that the user hasnt previously granted or denied permission for in this app), the system automatically displays the permission form, listing all the requested permissions. After the user has finished responding, this method calls its completion block on a background queue. If the user has already chosen to grant or prohibit access to all of the types specified, HealthKit calls the completion without prompting the user.
- Important: In watchOS 6 and later, this method displays the permission form on Apple Watch, enabling independent HealthKit apps. In watchOS 5 and earlier, this method prompts the user to authorize the app on their paired iPhone. For more information, see Creating Independent watchOS Apps.
Each data type has two separate permissions, one to read it and one to share it. You can make a single request, and include all the data types your app needs.
Customize the messages displayed on the permissions sheet by setting the following keys:
* ``NSHealthShareUsageDescription`` customizes the message for reading data.
* ``NSHealthUpdateUsageDescription`` customizes the message for writing data.
- Warning: You must set the usage keys, or your app will crash when you request authorization.
For projects created using Xcode 13 or later, set these keys in the Target Properties list on the apps Info tab. For projects created with Xcode 12 or earlier, set these keys in the apps Info.plist file. For more information, see [Information Property List](doc://com.apple.documentation/documentation/bundleresources/information_property_list).
After users have set the permissions for your app, they can always change them using either the Settings or the Health app. Your app appears in the Health apps Sources tab, even if the user didnt allow permission to read or share data.
- Parameter typesToShare: A set containing the data types you want to share. This set can contain any concrete subclass of the ``HKSampleType`` class (any of the ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes ). If the user grants permission, your app can create and save these data types to the HealthKit store.
- Parameter typesToRead: A set containing the data types you want to read. This set can contain any concrete subclass of the ``HKObjectType`` class (any of the ``HKCharacteristicType``, ``HKQuantityType``, ``HKCategoryType``, ``HKWorkoutType``, or ``HKCorrelationType`` classes). If the user grants permission, your app can read these data types from the HealthKit store.
- Parameter completion: A block called after the user finishes responding to the request. The system calls this block with the following parameters:
- Parameter success: A Boolean value that indicates whether the request succeeded. This value doesnt indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, its true.
- Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, its set to nil.
*/
func requestAuthorization(
toShare typesToShare: Set<HKSampleType>?,
read typesToRead: Set<HKObjectType>?,
completion: @escaping (Bool, Error?) -> Void
)
/**
Asynchronously requests permission to read a data type that requires per-object authorization (such as vision prescriptions).
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func requestPerObjectReadAuthorization(for objectType: HKObjectType, predicate: NSPredicate?) async throws
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
Some samples require per-object authorization. For these samples, people can select which ones your app can read on a sample-by-sample basis. By default, your app can read any of the per-object authorization samples that it has saved to the HealthKit store; however, you may not always have access to those samples. People can update the authorization status for any of these samples at any time.
Your app can begin by querying for any samples that it already has permission to read.
```
// Read the newest prescription from the HealthKit store.
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.visionPrescription()],
sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)],
limit: 1)
let prescription: HKVisionPrescription
do {
guard let result = try await queryDescriptor.result(for: store).first else {
print("*** No prescription found. ***")
return
}
prescription = result
} catch {
// Handle the error here.
fatalError("*** An error occurred while reading the most recent vision prescriptions: \(error.localizedDescription) ***")
}
```
Based on the results, you can then decide whether you need to request authorization for additional samples. Call `requestPerObjectReadAuthorization(for:predicate:completion:)` to prompt someone to modify the samples your app has access to read.
```
// Request authorization to read vision prescriptions.
do {
try await store.requestPerObjectReadAuthorization(for: .visionPrescriptionType(), predicate: nil)
} catch HKError.errorUserCanceled {
// Handle the user canceling the authorization request.
print("*** The user canceled the authorization request. ***")
return
} catch {
// Handle the error here.
fatalError("*** An error occurred while requesting permission to read vision prescriptions: \(error.localizedDescription) ***")
}
```
- Important: Using the ``requestAuthorization(toShare:read:)`` method to request read access to any data types that require per-object authorization fails with an ``HKError.Code.errorInvalidArgument`` error.
When your app calls this method, HealthKit displays an authorization sheet that asks for permission to read the samples that match the predicate and object type. The person using your app can then select individual samples to share with your app. The system always asks for permission, regardless of whether they previously granted it.
After the person responds, the system calls the callback handler on an arbitrary background queue.
- Parameter objectType: The data type you want to read.
- Parameter predicate: A predicate that further restricts the data type.
- Parameter completion: A completion handler that the system calls after the user responds to the request. The completion handler has the following parameters:
- Parameter success: A Boolean value that indicates whether the request succeeded. This value doesnt indicate whether the user actually granted permission. The parameter is false if an error occurred while processing the request; otherwise, its true.
- Parameter error: An error object. If an error occurred, this object contains information about the error; otherwise, the system passes `nil`.
*/
func requestPerObjectReadAuthorization(
for objectType: HKObjectType,
predicate: NSPredicate?,
completion: @escaping (Bool, Error?) -> Void
)
/**
Requests permission to save and read the data types specified by an extension.
- Important: You can call this method from synchronous code using a completion handler, as shown on this page, or you can call it as an asynchronous method that has the following declaration:
```
func handleAuthorizationForExtension() async throws
```
For information about concurrency and asynchronous code in Swift, see [Calling Objective-C APIs Asynchronously](doc://com.apple.documentation/documentation/swift/calling-objective-c-apis-asynchronously).
The host app must implement the application delegates ``applicationShouldRequestHealthAuthorization(_:)`` method.
This delegate method is called after an app extension calls ``requestAuthorization(toShare:read:completion:)``.
The host app is then responsible for calling `handleAuthorizationForExtension(completion:)`.
This method prompts the user to authorize both the app and its extensions for the types that the extension requested.
The system performs this request asynchronously.
After the user has finished responding, this method calls its completion block on a background queue.
If the user has already chosen to grant or prohibit access to all of the types specified, the completion is called without prompting the user.
- Parameters:
- completion: A block that is called after the user finishes responding to the request. This block is passed the following parameters:
- success: A Boolean value that indicates whether the user responded to the prompt (if any). This value does not indicate whether permission was actually granted. This parameter is false if the user canceled the prompt without selecting permissions; otherwise, true.
- error: An error object. If an error occurred, this object contains information about the error; otherwise, it is set to `nil`.
*/
func handleAuthorizationForExtension(completion: @escaping (Bool, Error?) -> Void)
/**
The view controller that presents HealthKit authorization sheets.
By default, the system infers the correct view controller to show HealthKits authorization sheet.
In some cases, you can improve the user experience by explicitly defining how the system presents the authentication sheets.
In particular, consider setting this property when using HealthKit in an iPadOS app.
*/
//@available(iOS 17.0, macCatalyst 17.0, visionOS 1.0, *)
//var authorizationViewControllerPresenter: UIViewController? { get set }
// MARK: - Querying HealthKit data
/**
Augments a `HKSampleQuery`.
This function performs the same action as when creating a `HKSampleQuery` and executing it on a `HKHealthStore`.
It's necessary to extract this function since not all properties of a `HKSampleType` can be accessed.
*/
func executeSampleQuery(sampleType: HKSampleType, predicate: NSPredicate?, limit: Int, sortDescriptors: [NSSortDescriptor]?, resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void) -> HKQuery
}

View File

@ -1,48 +1,60 @@
import SwiftUI
import HealthKit
import HKDatabase
import CoreLocation
struct ActivityDetailView: View {
@EnvironmentObject
var database: HealthDatabase
let activity: HKWorkoutActivity
@State var locations: [LocationSample] = []
@State var locations: [CLLocation] = []
@State var sampleCount: Int = 0
private var metadata: [(key: String, value: Any)] {
activity.metadata?.sorted { $0.key } ?? []
}
var body: some View {
List {
DetailRow("UUID", value: activity.uuid)
DetailRow("Activity", value: activity.workoutConfiguration.activityType)
DetailRow("Location", value: activity.workoutConfiguration.locationType)
DetailRow("Swimming Location", value: activity.workoutConfiguration.swimmingLocationType)
DetailRow("Lap Length", value: activity.workoutConfiguration.lapLength)
DetailRow("Start", date: activity.startDate)
DetailRow("End", date: activity.endDate)
DetailRow("Duration", duration: activity.duration)
DetailRow("Metadata", value: activity.metadata)
if !locations.isEmpty {
NavigationLink(value: locations) {
DetailRow("Locations", value: "\(locations.count)")
}
} else {
DetailRow("Locations", value: "0")
Section("Properties") {
DetailRow("UUID", value: activity.uuid)
DetailRow("Activity", value: activity.workoutConfiguration.activityType)
DetailRow("Location", value: activity.workoutConfiguration.locationType)
DetailRow("Swimming Location", value: activity.workoutConfiguration.swimmingLocationType)
DetailRow("Lap Length", value: activity.workoutConfiguration.lapLength)
DetailRow("Start", date: activity.startDate)
DetailRow("End", date: activity.endDate)
DetailRow("Duration", duration: activity.duration)
}
if sampleCount != 0 {
NavigationLink {
ActivitySamplesView(activity: activity)
.environmentObject(database)
} label: {
DetailRow("Samples", value: "\(sampleCount)")
Section("Data") {
if !locations.isEmpty {
NavigationLink(value: locations) {
DetailRow("Locations", value: "\(locations.count)")
}
} else {
DetailRow("Locations", value: "0")
}
if sampleCount != 0 {
NavigationLink {
ActivitySamplesView(activity: activity)
} label: {
DetailRow("Samples", value: "\(sampleCount)")
}
} else {
DetailRow("Samples", value: "0")
}
}
if !(activity.metadata?.isEmpty ?? true) {
Section("Metadata") {
ForEach(metadata, id: \.key) { (key, value) in
DetailRow(key, value: "\(value)")
}
}
} else {
DetailRow("Samples", value: "0")
}
}
.navigationTitle("Activity")
.navigationDestination(for: [LocationSample].self) { locations in
.navigationDestination(for: [CLLocation].self) { locations in
LocationSampleListView(samples: locations)
}
.onAppear(perform: load)
@ -51,12 +63,12 @@ struct ActivityDetailView: View {
private func load() {
Task {
do {
let samples = try database.locationSamples(for: activity)
let samples = try HealthDatabase.shared.locationSamples(for: activity)
.sorted { $0.timestamp }
let sampleCount = try database.sampleCount(for: activity)
//let sampleCount = try HealthDatabase.shared.sampleCount(for: activity)
DispatchQueue.main.async {
self.locations = samples
self.sampleCount = sampleCount
//self.sampleCount = sampleCount
}
} catch {
print("Failed to load location samples for activity: \(error)")
@ -66,8 +78,8 @@ struct ActivityDetailView: View {
}
#Preview {
NavigationStack {
HealthDatabase.shared = .mock()
return NavigationStack {
ActivityDetailView(activity: .mock1)
.environmentObject(HealthDatabase.mock())
}
}

View File

@ -1,15 +1,13 @@
import SwiftUI
import OrderedCollections
import HealthKit
import HKDatabase
struct ActivitySamplesView: View {
@EnvironmentObject
var database: HealthDatabase
let activity: HKWorkoutActivity
@State var samples: [(type: Sample.DataType, samples: [Sample])] = []
@State var samples: [(type: HKSampleType, samples: [HKSample])] = []
@State var timeZones: [TimeZone] = []
@ -45,13 +43,15 @@ struct ActivitySamplesView: View {
}
private func loadAsync() {
#warning("Load samples for activity")
/*
do {
let samples = try database.samples(for: activity)
let samples = try HealthDatabase.shared.samples(for: activity)
let ordered = samples
.sorted(using: { $0.key.rawValue })
.map { (type: $0, samples: $1) }
let timeZones: Set<TimeZone> = samples.reduce(into: Set()) { timeZones, sample in
timeZones.formUnion(sample.value.compactMap { $0.timeZone })
timeZones.formUnion(sample.compactMap { $0.timeZone })
}
DispatchQueue.main.async {
@ -62,6 +62,7 @@ struct ActivitySamplesView: View {
} catch {
print("Failed to load samples: \(error)")
}
*/
}
}

View File

@ -1,17 +1,18 @@
import SwiftUI
import HealthKit
import HKDatabase
struct ContentView: View {
@StateObject var database: HealthDatabase
@State var navigationPath: NavigationPath = .init()
@State var workouts: [Workout] = []
var body: some View {
NavigationStack(path: $navigationPath) {
VStack {
List {
ForEach(database.workouts) { workout in
ForEach(workouts) { workout in
NavigationLink(value: workout) {
VStack(alignment: .leading) {
Text(workout.typeString)
@ -26,12 +27,50 @@ struct ContentView: View {
.navigationTitle("Workouts")
.navigationDestination(for: Workout.self) {
WorkoutDetailView(workout: $0)
.environmentObject(database)
}
.toolbar {
ToolbarItem {
Button(action: addWorkouts) {
Text("Add")
}
}
}
}
}
private func addWorkouts() {
Task {
do {
try await insertExamplesOfAllTypes()
} catch {
print("Failed: \(error)")
}
}
}
}
#Preview {
ContentView(database: .mock())
HealthDatabase.shared = .mock()
return ContentView()
}
/*
data_series.hfd_key -> location_series_data.series_identifier
samples.data_id = data_series.data_id
samples.data_id = quantity_samples.data_id
samples.data_id = objects.data_id
samples.data_id = quantity_sample_series.data_id
objects.provenance = data_provenances.ROWID
data_provenances.tz_name
data_provenances.origin_device
Samples -> Quantity Sample Series -> Quantity Series Data
Samples -> Data Series
quantity_sample_series.hfd_key = quantity_series_data.series_identifier
*/

View File

@ -1,121 +0,0 @@
import Foundation
import SQLite
import CoreLocation
import HealthKit
typealias Database = Connection
final class HealthDatabase: ObservableObject {
private let fileUrl: URL
private let database: Connection
private let samples: SamplesTable
private let workoutsTable: WorkoutsTable
private let locationSamples: LocationSeriesDataTable
private let dataSeries: DataSeriesTable
@Published
var workouts: [Workout] = []
convenience init(fileUrl: URL) throws {
let database = try Connection(fileUrl.path)
self.init(fileUrl: fileUrl, database: database)
}
init(fileUrl: URL, database: Connection) {
self.fileUrl = fileUrl
self.database = database
self.samples = .init(database: database)
self.workoutsTable = .init(database: database)
self.locationSamples = .init(database: database)
self.dataSeries = .init(database: database)
DispatchQueue.global().async {
self.readAllWorkouts()
}
}
func readAllWorkouts() {
let workouts: [Workout]
do {
workouts = try workoutsTable.workouts()
} catch {
print("Failed to read workouts: \(error)")
return
}
DispatchQueue.main.async {
self.workouts = workouts
}
}
func locationSamples(for activity: HKWorkoutActivity) throws -> [LocationSample] {
try locationSamples.locationSamples(from: activity.startDate, to: activity.currentEndDate)
}
func locationSampleCount(for activity: HKWorkoutActivity) throws -> Int {
try locationSamples.locationSampleCount(from: activity.startDate, to: activity.currentEndDate)
}
func samples(for activity: HKWorkoutActivity) throws -> [Sample.DataType : [Sample]] {
try samples.samples(from: activity.startDate, to: activity.currentEndDate).reduce(into: [:]) {
$0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1]
}
}
func sampleCount(for activity: HKWorkoutActivity) throws -> Int {
try samples.sampleCount(from: activity.startDate, to: activity.currentEndDate)
}
var activities: [HKWorkoutActivity] {
workouts.map { $0.activities }.joined().sorted()
}
private func testActivityOverlap() {
let activities = self.activities
var current = activities.first!
for next in activities.dropFirst() {
let overlap = next.startDate.timeIntervalSince(current.currentEndDate)
if overlap < 0 {
print("Overlap \(-overlap.roundedInt) s:")
print(" Activity \(current.workoutConfiguration.activityType.description): \(current.startDate.timeAndDateText) -> \(current.currentEndDate.timeAndDateText)")
print(" Activity \(next.workoutConfiguration.activityType.description): \(next.startDate.timeAndDateText) -> \(next.currentEndDate.timeAndDateText)")
}
current = next
}
}
convenience init(database: Database) {
self.init(fileUrl: .init(filePath: "/"), database: database)
}
func insert(workout: Workout) throws {
try workoutsTable.insert(workout)
}
func insert(workout: Workout, into store: HKHealthStore) async throws -> HKWorkout? {
guard let configuration = workout.activities.first?.workoutConfiguration else {
return nil
}
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil)
return try await builder.finishWorkout()
}
func createTables() throws {
try samples.createAll()
try workoutsTable.createAll()
try locationSamples.create(references: dataSeries)
}
}
private extension HKWorkoutActivity {
var currentEndDate: Date {
endDate ?? Date()
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array/>
</dict>
</plist>

View File

@ -1,15 +1,23 @@
import SwiftUI
import HKDatabase
#warning("Load workouts from database and samples from HealthKit")
@main
struct HealthImportApp: App {
var body: some Scene {
WindowGroup {
ContentView(database: .init())
ContentView()
}
}
}
extension HealthDatabase {
static var shared: HealthDatabase = .init()
}
private extension HealthDatabase {
static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite")

View File

@ -1,8 +1,9 @@
import SwiftUI
import CoreLocation
struct LocationSampleDetailView: View {
let location: LocationSample
let location: CLLocation
var body: some View {
List {

View File

@ -1,8 +1,9 @@
import SwiftUI
import CoreLocation
struct LocationSampleListView: View {
let samples: [LocationSample]
let samples: [CLLocation]
var body: some View {
List {
@ -13,7 +14,7 @@ struct LocationSampleListView: View {
}
}
.navigationTitle("Locations")
.navigationDestination(for: LocationSample.self) {
.navigationDestination(for: CLLocation.self) {
LocationSampleDetailView(location: $0)
}
}

View File

@ -1,208 +0,0 @@
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: EventMetadata.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
import Foundation
import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2
}
/// Wrapper for workout event metadata
struct WorkoutEventMetadata {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// All metadata elements
var elements: [WorkoutEventMetadata.Element] = []
var unknownFields = SwiftProtobuf.UnknownStorage()
struct Element {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var key: String = String()
var unsignedValue: UInt64 {
get {return _unsignedValue ?? 0}
set {_unsignedValue = newValue}
}
/// Returns true if `unsignedValue` has been explicitly set.
var hasUnsignedValue: Bool {return self._unsignedValue != nil}
/// Clears the value of `unsignedValue`. Subsequent reads from it will return its default value.
mutating func clearUnsignedValue() {self._unsignedValue = nil}
var quantity: WorkoutEventMetadata.Element.Quantity {
get {return _quantity ?? WorkoutEventMetadata.Element.Quantity()}
set {_quantity = newValue}
}
/// Returns true if `quantity` has been explicitly set.
var hasQuantity: Bool {return self._quantity != nil}
/// Clears the value of `quantity`. Subsequent reads from it will return its default value.
mutating func clearQuantity() {self._quantity = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
struct Quantity {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var value: Double = 0
var unit: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
init() {}
fileprivate var _unsignedValue: UInt64? = nil
fileprivate var _quantity: WorkoutEventMetadata.Element.Quantity? = nil
}
init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension WorkoutEventMetadata: @unchecked Sendable {}
extension WorkoutEventMetadata.Element: @unchecked Sendable {}
extension WorkoutEventMetadata.Element.Quantity: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension WorkoutEventMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "WorkoutEventMetadata"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "elements"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.elements) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.elements.isEmpty {
try visitor.visitRepeatedMessageField(value: self.elements, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: WorkoutEventMetadata, rhs: WorkoutEventMetadata) -> Bool {
if lhs.elements != rhs.elements {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension WorkoutEventMetadata.Element: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = WorkoutEventMetadata.protoMessageName + ".Element"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "key"),
4: .same(proto: "unsignedValue"),
6: .same(proto: "quantity"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.key) }()
case 4: try { try decoder.decodeSingularUInt64Field(value: &self._unsignedValue) }()
case 6: try { try decoder.decodeSingularMessageField(value: &self._quantity) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.key.isEmpty {
try visitor.visitSingularStringField(value: self.key, fieldNumber: 1)
}
try { if let v = self._unsignedValue {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4)
} }()
try { if let v = self._quantity {
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
} }()
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: WorkoutEventMetadata.Element, rhs: WorkoutEventMetadata.Element) -> Bool {
if lhs.key != rhs.key {return false}
if lhs._unsignedValue != rhs._unsignedValue {return false}
if lhs._quantity != rhs._quantity {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension WorkoutEventMetadata.Element.Quantity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = WorkoutEventMetadata.Element.protoMessageName + ".Quantity"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "value"),
2: .same(proto: "unit"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularDoubleField(value: &self.value) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.unit) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.value != 0 {
try visitor.visitSingularDoubleField(value: self.value, fieldNumber: 1)
}
if !self.unit.isEmpty {
try visitor.visitSingularStringField(value: self.unit, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: WorkoutEventMetadata.Element.Quantity, rhs: WorkoutEventMetadata.Element.Quantity) -> Bool {
if lhs.value != rhs.value {return false}
if lhs.unit != rhs.unit {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View File

@ -1,46 +0,0 @@
import Foundation
enum Goal {
case time(TimeInterval)
init?(goalType: Int?, goal: Double?) {
switch goalType {
case .none:
return nil
case 0:
return nil
case 2:
guard let goal else {
print("Time goal, but no goal value set")
return nil
}
self = .time(goal)
default:
return nil
}
}
var gaol: Double {
switch self {
case .time(let timeInterval):
return timeInterval
}
}
var goalType: Int {
switch self {
case .time: return 2
}
}
}
extension Goal: CustomStringConvertible {
var description: String {
switch self {
case .time(let seconds):
return seconds.durationString
}
}
}

View File

@ -1,19 +0,0 @@
import Foundation
import HealthKit
extension HKWorkoutActivity: Comparable {
public static func < (lhs: HKWorkoutActivity, rhs: HKWorkoutActivity) -> Bool {
lhs.startDate < rhs.startDate
}
}
extension HKWorkoutActivity {
var externalUUID: UUID? {
guard let string = metadata?[HKMetadataKeyExternalUUID] as? String else {
return nil
}
return UUID(uuidString: string)
}
}

View File

@ -1,10 +0,0 @@
import Foundation
import HealthKit
import SwiftProtobuf
extension HKWorkoutEvent: Identifiable {
public var id: Double {
dateInterval.start.timeIntervalSinceReferenceDate * Double(type.rawValue) * dateInterval.duration
}
}

View File

@ -1,329 +0,0 @@
import Foundation
enum Metadata {
}
extension Metadata {
enum Key {
case wasUserEntered
case heartRateSensorLocation
case sessionId
case timeZone
case subIndex
case indoorWorkout
case privateMediaSourceBundleIdentifier
case sexualActivityProtectionUsed
case privateSleepAlarmUserWakeTime
case privateSleepAlarmUserSetBedtime
case devicePlacementSide
case privateHeadphoneAudioDataIsTransient
case privateCoreMotionSourceIdentifier
case privateHeartRateContext
case algorithmVersion
case privateAppleHeartbeatSeriesAlgorithmVersion
case barometricPressure
case privateBloodOxygenContext
case privateHeartbeatSequenceContext
case appleECGAlgorithmVersion
case privateRegulatedUpdateVersion
case privateActivitySummaryIndex
case privateDeepBreathingEndReason
case privateDeepBreathingFinalHeartRate
case privateMindfulnessSessionType
case privateWorkoutAverageHeartRate
case privateWorkoutMaxHeartRate
case privateWeatherCondition
case privateWorkoutWeatherLocationCoordinatesLongitude
case privateWorkoutMinHeartRate
case privateWorkoutWasInDaytime
case privateWorkoutActivityMoveMode
case averageMETs
case privateWorkoutWeatherLocationCoordinatesLatitude
case privateLostGPSAtSomePoint
case weatherTemperature
case privateWorkoutMinGroundElevation
case weatherHumidity
case privateWorkoutMaxGroundElevation
case elevationAscended
case privateWorkoutAverageCadence
case vO2MaxTestType
case privateUserOnBetaBlocker
case swimmingLocationType
case lapLength
case swimmingStrokeStyle
case audioExposureLevel
case privateAudioExposureLimit
case dateOfEarliestDataUsedForEstimate
case appleDeviceCalibrated
case heartRateEventThreshold
case privateHeartRateEventThreshold
case privateWorkoutTargetZoneMax
case privateWorkoutTargetZoneType
case privateWorkoutTargetZoneMin
case privateFallActionRequested
case glassesPrescriptionDescription
case privateWorkoutElapsedTimeInHeartRateZones
case privateWorkoutWeatherSourceName
case privateWorkoutHeartRateZones
case privateWorkoutConfiguration
case privateWorkoutHeartRateZonesConfigurationType
case privateWorkoutAveragePower
case privateMetricPlatterStatistics
case userMotionContext
case heartRateRecoveryActivityDuration
case heartRateRecoveryTestType
case heartRateRecoveryMaxObservedRecoveryHeartRate
case heartRateRecoveryActivityType
case sessionEstimate
case privateWorkoutExtendedMode
case unknown(String)
}
}
extension Metadata.Key: RawRepresentable {
init(rawValue: String) {
switch rawValue {
case "HKWasUserEntered": self = .wasUserEntered
case "HKHeartRateSensorLocation": self = .heartRateSensorLocation
case "sessionId": self = .sessionId
case "HKTimeZone": self = .timeZone
case "subIndex": self = .subIndex
case "HKIndoorWorkout": self = .indoorWorkout
case "_HKPrivateMediaSourceBundleIdentifier": self = .privateMediaSourceBundleIdentifier
case "HKSexualActivityProtectionUsed": self = .sexualActivityProtectionUsed
case "_HKPrivateSleepAlarmUserWakeTime": self = .privateSleepAlarmUserWakeTime
case "_HKPrivateSleepAlarmUserSetBedtime": self = .privateSleepAlarmUserSetBedtime
case "HKMetadataKeyDevicePlacementSide": self = .devicePlacementSide
case "_HKPrivateMetadataKeyHeadphoneAudioDataIsTransient": self = .privateHeadphoneAudioDataIsTransient
case "_HKPrivateCoreMotionSourceIdentifier": self = .privateCoreMotionSourceIdentifier
case "_HKPrivateHeartRateContext": self = .privateHeartRateContext
case "HKAlgorithmVersion": self = .algorithmVersion
case "_HKPrivateMetadataKeyAppleHeartbeatSeriesAlgorithmVersion": self = .privateAppleHeartbeatSeriesAlgorithmVersion
case "HKMetadataKeyBarometricPressure": self = .barometricPressure
case "_HKPrivateBloodOxygenContext": self = .privateBloodOxygenContext
case "_HKPrivateHeartbeatSequenceContext": self = .privateHeartbeatSequenceContext
case "HKMetadataKeyAppleECGAlgorithmVersion": self = .appleECGAlgorithmVersion
case "_HKPrivateMetadataKeyRegulatedUpdateVersion": self = .privateRegulatedUpdateVersion
case "_HKPrivateActivitySummaryIndex": self = .privateActivitySummaryIndex
case "_HKPrivateDeepBreathingEndReason": self = .privateDeepBreathingEndReason
case "_HKPrivateDeepBreathingFinalHeartRate": self = .privateDeepBreathingFinalHeartRate
case "_HKPrivateMetadataMindfulnessSessionType": self = .privateMindfulnessSessionType
case "_HKPrivateWorkoutAverageHeartRate": self = .privateWorkoutAverageHeartRate
case "_HKPrivateWorkoutMaxHeartRate": self = .privateWorkoutMaxHeartRate
case "_HKPrivateWeatherCondition": self = .privateWeatherCondition
case "_HKPrivateWorkoutWeatherLocationCoordinatesLongitude": self = .privateWorkoutWeatherLocationCoordinatesLongitude
case "_HKPrivateWorkoutMinHeartRate": self = .privateWorkoutMinHeartRate
case "_HKPrivateWorkoutWasInDaytime": self = .privateWorkoutWasInDaytime
case "_HKPrivateWorkoutActivityMoveMode": self = .privateWorkoutActivityMoveMode
case "HKAverageMETs": self = .averageMETs
case "_HKPrivateWorkoutWeatherLocationCoordinatesLatitude": self = .privateWorkoutWeatherLocationCoordinatesLatitude
case "_HKPrivateMetadataKeyLostGPSAtSomePoint": self = .privateLostGPSAtSomePoint
case "HKWeatherTemperature": self = .weatherTemperature
case "_HKPrivateWorkoutMinGroundElevation": self = .privateWorkoutMinGroundElevation
case "HKWeatherHumidity": self = .weatherHumidity
case "_HKPrivateWorkoutMaxGroundElevation": self = .privateWorkoutMaxGroundElevation
case "HKElevationAscended": self = .elevationAscended
case "_HKPrivateWorkoutAverageCadence": self = .privateWorkoutAverageCadence
case "HKVO2MaxTestType": self = .vO2MaxTestType
case "_HKPrivateMetadataKeyUserOnBetaBlocker": self = .privateUserOnBetaBlocker
case "HKSwimmingLocationType": self = .swimmingLocationType
case "HKLapLength": self = .lapLength
case "HKSwimmingStrokeStyle": self = .swimmingStrokeStyle
case "HKMetadataKeyAudioExposureLevel": self = .audioExposureLevel
case "_HKPrivateMetadataKeyAudioExposureLimit": self = .privateAudioExposureLimit
case "HKDateOfEarliestDataUsedForEstimate": self = .dateOfEarliestDataUsedForEstimate
case "HKMetadataKeyAppleDeviceCalibrated": self = .appleDeviceCalibrated
case "HKHeartRateEventThreshold": self = .heartRateEventThreshold
case "_HKPrivateMetadataKeyHeartRateEventThreshold": self = .privateHeartRateEventThreshold
case "_HKPrivateWorkoutTargetZoneMax": self = .privateWorkoutTargetZoneMax
case "_HKPrivateWorkoutTargetZoneType": self = .privateWorkoutTargetZoneType
case "_HKPrivateWorkoutTargetZoneMin": self = .privateWorkoutTargetZoneMin
case "_HKPrivateFallActionRequested": self = .privateFallActionRequested
case "HKMetadataKeyGlassesPrescriptionDescription": self = .glassesPrescriptionDescription
case "_HKPrivateWorkoutElapsedTimeInHeartRateZones": self = .privateWorkoutElapsedTimeInHeartRateZones
case "_HKPrivateWorkoutWeatherSourceName": self = .privateWorkoutWeatherSourceName
case "_HKPrivateWorkoutHeartRateZones": self = .privateWorkoutHeartRateZones
case "_HKPrivateWorkoutConfiguration": self = .privateWorkoutConfiguration
case "_HKPrivateWorkoutHeartRateZonesConfigurationType": self = .privateWorkoutHeartRateZonesConfigurationType
case "_HKPrivateWorkoutAveragePower": self = .privateWorkoutAveragePower
case "_HKPrivateMetadataKeyMetricPlatterStatistics": self = .privateMetricPlatterStatistics
case "HKMetadataKeyUserMotionContext": self = .userMotionContext
case "HKMetadataKeyHeartRateRecoveryActivityDuration": self = .heartRateRecoveryActivityDuration
case "HKMetadataKeyHeartRateRecoveryTestType": self = .heartRateRecoveryTestType
case "HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate": self = .heartRateRecoveryMaxObservedRecoveryHeartRate
case "HKMetadataKeyHeartRateRecoveryActivityType": self = .heartRateRecoveryActivityType
case "HKMetadataKeySessionEstimate": self = .sessionEstimate
case "_HKPrivateWorkoutExtendedMode": self = .privateWorkoutExtendedMode
default:
self = .unknown(rawValue)
}
}
var rawValue: String {
switch self {
case .wasUserEntered: return "HKWasUserEntered"
case .heartRateSensorLocation: return "HKHeartRateSensorLocation"
case .sessionId: return "sessionId"
case .timeZone: return "HKTimeZone"
case .subIndex: return "subIndex"
case .indoorWorkout: return "HKIndoorWorkout"
case .privateMediaSourceBundleIdentifier: return "_HKPrivateMediaSourceBundleIdentifier"
case .sexualActivityProtectionUsed: return "HKSexualActivityProtectionUsed"
case .privateSleepAlarmUserWakeTime: return "_HKPrivateSleepAlarmUserWakeTime"
case .privateSleepAlarmUserSetBedtime: return "_HKPrivateSleepAlarmUserSetBedtime"
case .devicePlacementSide: return "HKMetadataKeyDevicePlacementSide"
case .privateHeadphoneAudioDataIsTransient: return "_HKPrivateMetadataKeyHeadphoneAudioDataIsTransient"
case .privateCoreMotionSourceIdentifier: return "_HKPrivateCoreMotionSourceIdentifier"
case .privateHeartRateContext: return "_HKPrivateHeartRateContext"
case .algorithmVersion: return "HKAlgorithmVersion"
case .privateAppleHeartbeatSeriesAlgorithmVersion: return "_HKPrivateMetadataKeyAppleHeartbeatSeriesAlgorithmVersion"
case .barometricPressure: return "HKMetadataKeyBarometricPressure"
case .privateBloodOxygenContext: return "_HKPrivateBloodOxygenContext"
case .privateHeartbeatSequenceContext: return "_HKPrivateHeartbeatSequenceContext"
case .appleECGAlgorithmVersion: return "HKMetadataKeyAppleECGAlgorithmVersion"
case .privateRegulatedUpdateVersion: return "_HKPrivateMetadataKeyRegulatedUpdateVersion"
case .privateActivitySummaryIndex: return "_HKPrivateActivitySummaryIndex"
case .privateDeepBreathingEndReason: return "_HKPrivateDeepBreathingEndReason"
case .privateDeepBreathingFinalHeartRate: return "_HKPrivateDeepBreathingFinalHeartRate"
case .privateMindfulnessSessionType: return "_HKPrivateMetadataMindfulnessSessionType"
case .privateWorkoutAverageHeartRate: return "_HKPrivateWorkoutAverageHeartRate"
case .privateWorkoutMaxHeartRate: return "_HKPrivateWorkoutMaxHeartRate"
case .privateWeatherCondition: return "_HKPrivateWeatherCondition"
case .privateWorkoutWeatherLocationCoordinatesLongitude: return "_HKPrivateWorkoutWeatherLocationCoordinatesLongitude"
case .privateWorkoutMinHeartRate: return "_HKPrivateWorkoutMinHeartRate"
case .privateWorkoutWasInDaytime: return "_HKPrivateWorkoutWasInDaytime"
case .privateWorkoutActivityMoveMode: return "_HKPrivateWorkoutActivityMoveMode"
case .averageMETs: return "HKAverageMETs"
case .privateWorkoutWeatherLocationCoordinatesLatitude: return "_HKPrivateWorkoutWeatherLocationCoordinatesLatitude"
case .privateLostGPSAtSomePoint: return "_HKPrivateMetadataKeyLostGPSAtSomePoint"
case .weatherTemperature: return "HKWeatherTemperature"
case .privateWorkoutMinGroundElevation: return "_HKPrivateWorkoutMinGroundElevation"
case .weatherHumidity: return "HKWeatherHumidity"
case .privateWorkoutMaxGroundElevation: return "_HKPrivateWorkoutMaxGroundElevation"
case .elevationAscended: return "HKElevationAscended"
case .privateWorkoutAverageCadence: return "_HKPrivateWorkoutAverageCadence"
case .vO2MaxTestType: return "HKVO2MaxTestType"
case .privateUserOnBetaBlocker: return "_HKPrivateMetadataKeyUserOnBetaBlocker"
case .swimmingLocationType: return "HKSwimmingLocationType"
case .lapLength: return "HKLapLength"
case .swimmingStrokeStyle: return "HKSwimmingStrokeStyle"
case .audioExposureLevel: return "HKMetadataKeyAudioExposureLevel"
case .privateAudioExposureLimit: return "_HKPrivateMetadataKeyAudioExposureLimit"
case .dateOfEarliestDataUsedForEstimate: return "HKDateOfEarliestDataUsedForEstimate"
case .appleDeviceCalibrated: return "HKMetadataKeyAppleDeviceCalibrated"
case .heartRateEventThreshold: return "HKHeartRateEventThreshold"
case .privateHeartRateEventThreshold: return "_HKPrivateMetadataKeyHeartRateEventThreshold"
case .privateWorkoutTargetZoneMax: return "_HKPrivateWorkoutTargetZoneMax"
case .privateWorkoutTargetZoneType: return "_HKPrivateWorkoutTargetZoneType"
case .privateWorkoutTargetZoneMin: return "_HKPrivateWorkoutTargetZoneMin"
case .privateFallActionRequested: return "_HKPrivateFallActionRequested"
case .glassesPrescriptionDescription: return "HKMetadataKeyGlassesPrescriptionDescription"
case .privateWorkoutElapsedTimeInHeartRateZones: return "_HKPrivateWorkoutElapsedTimeInHeartRateZones"
case .privateWorkoutWeatherSourceName: return "_HKPrivateWorkoutWeatherSourceName"
case .privateWorkoutHeartRateZones: return "_HKPrivateWorkoutHeartRateZones"
case .privateWorkoutConfiguration: return "_HKPrivateWorkoutConfiguration"
case .privateWorkoutHeartRateZonesConfigurationType: return "_HKPrivateWorkoutHeartRateZonesConfigurationType"
case .privateWorkoutAveragePower: return "_HKPrivateWorkoutAveragePower"
case .privateMetricPlatterStatistics: return "_HKPrivateMetadataKeyMetricPlatterStatistics"
case .userMotionContext: return "HKMetadataKeyUserMotionContext"
case .heartRateRecoveryActivityDuration: return "HKMetadataKeyHeartRateRecoveryActivityDuration"
case .heartRateRecoveryTestType: return "HKMetadataKeyHeartRateRecoveryTestType"
case .heartRateRecoveryMaxObservedRecoveryHeartRate: return "HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate"
case .heartRateRecoveryActivityType: return "HKMetadataKeyHeartRateRecoveryActivityType"
case .sessionEstimate: return "HKMetadataKeySessionEstimate"
case .privateWorkoutExtendedMode: return "_HKPrivateWorkoutExtendedMode"
case .unknown(let rawValue):
return rawValue
}
}
}
extension Metadata.Key: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
}
extension Metadata.Key: CustomStringConvertible {
var description: String {
switch self {
case .wasUserEntered: return "Entered by user"
case .heartRateSensorLocation: return "Location of Heart Rate Sensor"
case .sessionId: return "Session ID"
case .timeZone: return "Timezone"
case .subIndex: return "Sub-Index"
case .indoorWorkout: return "Indoor Workout"
case .sexualActivityProtectionUsed: return "Protection Used for Sexual Activity"
case .privateMediaSourceBundleIdentifier: return "Media Source Bundle Identifier (private)"
case .privateSleepAlarmUserWakeTime: return "Sleep Alarm User Wake Time (private)"
case .privateSleepAlarmUserSetBedtime: return "Sleep Alarm User Set Bedtime (private)"
case .devicePlacementSide: return "Device Placement Side"
case .privateHeadphoneAudioDataIsTransient: return "Headphone Audio Data is Transient (private)"
case .privateCoreMotionSourceIdentifier: return "Core Motion Source Identifier (private)"
case .privateHeartRateContext: return "Heart Rate Context (private)"
case .algorithmVersion: return "Algorithm Version"
case .privateAppleHeartbeatSeriesAlgorithmVersion: return "Apple Heartbeat Series Algorithm Version (private)"
case .barometricPressure: return "Barometric Pressure"
case .privateBloodOxygenContext: return "Blood Oxygen Context (private)"
case .privateHeartbeatSequenceContext: return "Heartbeat Sequence Context (private)"
case .appleECGAlgorithmVersion: return "Apple ECG Algorithm Version"
case .privateRegulatedUpdateVersion: return "Regulated Update Version (private)"
case .privateActivitySummaryIndex: return "Activity Summary Index (private)"
case .privateDeepBreathingEndReason: return "Reason for End of Deep Breathing (private)"
case .privateDeepBreathingFinalHeartRate: return "Deep Breathing Final Heart Rate (private)"
case .privateMindfulnessSessionType: return "Mindfulness Session Type (private)"
case .privateWorkoutAverageHeartRate: return "Average Workout Heart Rate (private)"
case .privateWorkoutMaxHeartRate: return "Maximum Workout Heart Rate (private)"
case .privateWorkoutMinHeartRate: return "Minimum Workout Heart Rate (private)"
case .privateWorkoutWasInDaytime: return "Workout Was in Daytime (private)"
case .privateWorkoutActivityMoveMode: return "Workout Activity Move Mode (private)"
case .averageMETs: return "Average METs"
case .privateWorkoutWeatherLocationCoordinatesLatitude: return "Workout Weather Location Latitude (private)"
case .privateWorkoutWeatherLocationCoordinatesLongitude: return "Workout Weather Location Longitude (private)"
case .privateLostGPSAtSomePoint: return "Lost GPS At Some Point (private)"
case .privateWeatherCondition: return "Weather Condition (private)"
case .weatherTemperature: return "Weather Temperature"
case .weatherHumidity: return "Weather Humidity"
case .privateWorkoutMinGroundElevation: return "Minimum Ground Elevation for Workout (private)"
case .privateWorkoutMaxGroundElevation: return "Maximum Ground Elevation for Workout (private)"
case .elevationAscended: return "Ascended Elevation"
case .privateWorkoutAverageCadence: return "Average Workout Cadence (private)"
case .vO2MaxTestType: return "VO2Max Test Type"
case .privateUserOnBetaBlocker: return "User is on Beta Blocker (private)"
case .swimmingLocationType: return "Swimming Location Type"
case .lapLength: return "Lap Length"
case .swimmingStrokeStyle: return "Swimming Stroke Style"
case .audioExposureLevel: return "Audio Exposure Level"
case .privateAudioExposureLimit: return "Audio Exposure Limit (private)"
case .dateOfEarliestDataUsedForEstimate: return "Date of Earliest Data Used for Estimate"
case .appleDeviceCalibrated: return "Apple Device Calibrated"
case .heartRateEventThreshold: return "Heart Rate Event Threshold"
case .privateHeartRateEventThreshold: return "Heart Rate Event Threshold (private)"
case .privateWorkoutTargetZoneMax: return "Workout Target Zone Maximum (private)"
case .privateWorkoutTargetZoneType: return "Workout Target Zone Type (private)"
case .privateWorkoutTargetZoneMin: return "Workout Target Zone Minimum (private)"
case .privateFallActionRequested: return "Requested Fall Action (private)"
case .glassesPrescriptionDescription: return "Glasses Prescription Description"
case .privateWorkoutElapsedTimeInHeartRateZones: return "Workout Elapsed Time in Heart Rate Zones (private)"
case .privateWorkoutWeatherSourceName: return "Workout Weather Source Name (private)"
case .privateWorkoutHeartRateZones: return "Workout Heart Rate Zones (private)"
case .privateWorkoutConfiguration: return "Workout Configuration (private)"
case .privateWorkoutHeartRateZonesConfigurationType: return "Workout Heart Rate Zones Configuration Type (private)"
case .privateWorkoutAveragePower: return "Average Workout Power (private)"
case .privateMetricPlatterStatistics: return "Metric Platter Statistics (private)"
case .userMotionContext: return "User Motion Context"
case .heartRateRecoveryActivityDuration: return "Heart Rate Recovery Activity Duration"
case .heartRateRecoveryActivityType: return "Heart Rate Recovery Activity Type"
case .heartRateRecoveryTestType: return "Heart Rate Recovery Test Type"
case .heartRateRecoveryMaxObservedRecoveryHeartRate: return "Max. Observed Recovery Heart Rate"
case .sessionEstimate: return "Session Estimate"
case .privateWorkoutExtendedMode: return "Extended Workout Mode (private)"
case .unknown(let string): return string
}
}
}

View File

@ -1,59 +0,0 @@
import Foundation
extension Metadata {
enum Value {
case string(value: String)
case number(value: Double)
case date(value: Date)
case numerical(value: Double, unit: String)
case data(value: Data)
enum ValueType: Int {
/// Uses only the `string_value` column
case string = 0
/// Uses only the `numerical_value` column
case number = 1
/// Uses only the `date_value` column
case date = 2
/// Uses the `string_value` column for the unit, and the `numerical_value` column for the number
case numerical = 3
/// Uses only the `data_value` column
case data = 4
}
var valueType: ValueType {
switch self {
case .string: return .string
case .number: return .number
case .date: return .date
case .numerical: return .numerical
case .data: return .data
}
}
}
}
extension Metadata.Value: CustomStringConvertible {
var description: String {
switch self {
case .string(let value):
return value
case .number(let value):
return "\(value)"
case .date(let value):
return value.timeAndDateText
case .numerical(let value, let unit):
return String(format: "%.3f ", value) + unit
case .data(let value):
return value.description
}
}
}

View File

@ -1,146 +0,0 @@
import Foundation
struct Sample {
let startDate: Date
let endDate: Date
let dataType: DataType
let quantity: Double?
let originalQuantity: Double?
let originalUnit: String?
let timeZoneName: String?
var timeZone: TimeZone? {
guard let timeZoneName else {
return nil
}
guard let zone = TimeZone(identifier: timeZoneName) else {
print("No time zone for '\(timeZoneName)'")
return nil
}
return zone
}
var duration: TimeInterval {
endDate.timeIntervalSince(startDate)
}
var originalQuantityText: String {
guard let originalQuantity, let originalUnit else {
return ""
}
return " (\(originalQuantity) \(originalUnit))"
}
var quantityText: String {
guard let quantity else {
return "-"
}
return "\(quantity)"
}
var dateText: String {
startDate.timeAndDateText(in: timeZone ?? .current)
}
}
extension Sample: CustomStringConvertible {
var description: String {
"\(dateText) (\(Int(duration)) s) \(quantityText)\(originalQuantityText)"
}
}
extension Sample {
enum DataType: RawRepresentable {
case weight // 3
case heartRate // 5
case stepCount // 7
case distance // 8
case restingEnergy // 9
case activeEnergy // 10
case flightsClimed // 12
case weeklyCalorieGoal // 67
case watchOn // 70
case standMinutes // 75
case activity // 76
case workout // 79
case unknown(Int)
init(rawValue: Int) {
switch rawValue {
case 3: self = .weight
case 5: self = .heartRate
case 7: self = .stepCount
case 8: self = .distance
case 9: self = .restingEnergy
case 10: self = .activeEnergy
case 12: self = .flightsClimed
case 67: self = .weeklyCalorieGoal
case 70: self = .watchOn
case 75: self = .standMinutes
case 76: self = .activity
case 79: self = .workout
default:
self = .unknown(rawValue)
}
}
var rawValue: Int {
switch self {
case .stepCount: return 7
case .weight: return 3
case .heartRate: return 5
case .distance: return 8
case .restingEnergy: return 9
case .activeEnergy: return 10
case .flightsClimed: return 12
case .weeklyCalorieGoal: return 67
case .watchOn: return 70
case .standMinutes: return 75
case .activity: return 76
case .workout: return 79
case .unknown(let value): return value
}
}
}
}
extension Sample.DataType: Equatable {
}
extension Sample.DataType: Hashable {
}
extension Sample.DataType: CustomStringConvertible {
var description: String {
switch self {
case .stepCount: return "Step Count"
case .weight: return "Weight"
case .heartRate: return "Heart Rate"
case .distance: return "Distance"
case .restingEnergy: return "Resting Energy"
case .activeEnergy: return "Active Energy"
case .flightsClimed: return "Flights Climbed"
case .weeklyCalorieGoal: return "Weekly Calorie Goal"
case .watchOn: return "Watch On"
case .standMinutes: return "Stand Minutes"
case .activity: return "Activity"
case .workout: return "Workout"
case .unknown(let int): return "Unknown(\(int))"
}
}
}

View File

@ -1,266 +0,0 @@
import Foundation
import SQLite
import HealthKit
struct DataProvenancesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
func create() throws {
try database.execute("CREATE TABLE data_provenances (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, sync_provenance INTEGER NOT NULL, origin_product_type TEXT NOT NULL, origin_build TEXT NOT NULL, local_product_type TEXT NOT NULL, local_build TEXT NOT NULL, source_id INTEGER NOT NULL, device_id INTEGER NOT NULL, contributor_id INTEGER NOT NULL, source_version TEXT NOT NULL, tz_name TEXT NOT NULL, origin_major_version INTEGER NOT NULL, origin_minor_version INTEGER NOT NULL, origin_patch_version INTEGER NOT NULL, sync_identity INTEGER NOT NULL, derived_flags INTEGER NOT NULL, UNIQUE(sync_provenance, origin_product_type, origin_build, local_product_type, local_build, source_id, device_id, contributor_id, source_version, tz_name, origin_major_version, origin_minor_version, origin_patch_version, sync_identity))")
}
let table = Table("data_provenances")
let rowId = Expression<Int>("ROWID")
let syncProvenance = Expression<Int>("sync_provenance")
/// Device that created the data (e.g. Watch)
let originProductType = Expression<String>("origin_product_type")
let originBuild = Expression<String>("origin_build")
/// Device saving the data (e.g. iPhone)
let localProductType = Expression<String>("local_product_type")
let localBuild = Expression<String>("local_build")
let sourceId = Expression<Int>("source_id")
let deviceId = Expression<Int>("device_id")
let contributorId = Expression<Int>("contributor_id")
let sourceVersion = Expression<String>("source_version")
let tzName = Expression<String>("tz_name")
let originMajorVersion = Expression<Int>("origin_major_version")
let originMinorVersion = Expression<Int>("origin_minor_version")
let originPatchVersion = Expression<Int>("origin_patch_version")
let syncIdentity = Expression<Int>("sync_identity")
let derivedFlags = Expression<Int>("derived_flags")
func device(for rowId: Int) throws -> HKDevice? {
try database.pluck(table.filter(self.rowId == rowId)).map { row in
let productType = row[originProductType]
return HKDevice(
name: nil,
manufacturer: "Apple Inc.",
model: productTypeToHumanName[productType],
hardwareVersion: productType,
firmwareVersion: nil,
softwareVersion: "\(row[originMajorVersion]).\(row[originMinorVersion]).\(row[originPatchVersion])",
localIdentifier: nil,
udiDeviceIdentifier: nil)
}
}
func timeZoneName(for rowId: Int) throws -> String? {
try database.pluck(table.filter(self.rowId == rowId)).map { $0[tzName] }
}
}
private let productTypeToHumanName: [String : String] = [
"i386" : "iPhone Simulator",
"x86_64" : "iPhone Simulator",
"arm64" : "iPhone Simulator",
"iPhone1,1" : "iPhone",
"iPhone1,2" : "iPhone 3G",
"iPhone2,1" : "iPhone 3GS",
"iPhone3,1" : "iPhone 4",
"iPhone3,2" : "iPhone 4 GSM Rev A",
"iPhone3,3" : "iPhone 4 CDMA",
"iPhone4,1" : "iPhone 4S",
"iPhone5,1" : "iPhone 5 (GSM)",
"iPhone5,2" : "iPhone 5 (GSM+CDMA)",
"iPhone5,3" : "iPhone 5C (GSM)",
"iPhone5,4" : "iPhone 5C (Global)",
"iPhone6,1" : "iPhone 5S (GSM)",
"iPhone6,2" : "iPhone 5S (Global)",
"iPhone7,1" : "iPhone 6 Plus",
"iPhone7,2" : "iPhone 6",
"iPhone8,1" : "iPhone 6s",
"iPhone8,2" : "iPhone 6s Plus",
"iPhone8,4" : "iPhone SE (GSM)",
"iPhone9,1" : "iPhone 7",
"iPhone9,2" : "iPhone 7 Plus",
"iPhone9,3" : "iPhone 7",
"iPhone9,4" : "iPhone 7 Plus",
"iPhone10,1" : "iPhone 8",
"iPhone10,2" : "iPhone 8 Plus",
"iPhone10,3" : "iPhone X Global",
"iPhone10,4" : "iPhone 8",
"iPhone10,5" : "iPhone 8 Plus",
"iPhone10,6" : "iPhone X GSM",
"iPhone11,2" : "iPhone XS",
"iPhone11,4" : "iPhone XS Max",
"iPhone11,6" : "iPhone XS Max Global",
"iPhone11,8" : "iPhone XR",
"iPhone12,1" : "iPhone 11",
"iPhone12,3" : "iPhone 11 Pro",
"iPhone12,5" : "iPhone 11 Pro Max",
"iPhone12,8" : "iPhone SE 2nd Gen",
"iPhone13,1" : "iPhone 12 Mini",
"iPhone13,2" : "iPhone 12",
"iPhone13,3" : "iPhone 12 Pro",
"iPhone13,4" : "iPhone 12 Pro Max",
"iPhone14,2" : "iPhone 13 Pro",
"iPhone14,3" : "iPhone 13 Pro Max",
"iPhone14,4" : "iPhone 13 Mini",
"iPhone14,5" : "iPhone 13",
"iPhone14,6" : "iPhone SE 3rd Gen",
"iPhone14,7" : "iPhone 14",
"iPhone14,8" : "iPhone 14 Plus",
"iPhone15,2" : "iPhone 14 Pro",
"iPhone15,3" : "iPhone 14 Pro Max",
"iPhone15,4" : "iPhone 15",
"iPhone15,5" : "iPhone 15 Plus",
"iPhone16,1" : "iPhone 15 Pro",
"iPhone16,2" : "iPhone 15 Pro Max",
"iPod1,1" : "1st Gen iPod",
"iPod2,1" : "2nd Gen iPod",
"iPod3,1" : "3rd Gen iPod",
"iPod4,1" : "4th Gen iPod",
"iPod5,1" : "5th Gen iPod",
"iPod7,1" : "6th Gen iPod",
"iPod9,1" : "7th Gen iPod",
"iPad1,1" : "iPad",
"iPad1,2" : "iPad 3G",
"iPad2,1" : "2nd Gen iPad",
"iPad2,2" : "2nd Gen iPad GSM",
"iPad2,3" : "2nd Gen iPad CDMA",
"iPad2,4" : "2nd Gen iPad New Revision",
"iPad3,1" : "3rd Gen iPad",
"iPad3,2" : "3rd Gen iPad CDMA",
"iPad3,3" : "3rd Gen iPad GSM",
"iPad2,5" : "iPad mini",
"iPad2,6" : "iPad mini GSM+LTE",
"iPad2,7" : "iPad mini CDMA+LTE",
"iPad3,4" : "4th Gen iPad",
"iPad3,5" : "4th Gen iPad GSM+LTE",
"iPad3,6" : "4th Gen iPad CDMA+LTE",
"iPad4,1" : "iPad Air (WiFi)",
"iPad4,2" : "iPad Air (GSM+CDMA)",
"iPad4,3" : "1st Gen iPad Air (China)",
"iPad4,4" : "iPad mini Retina (WiFi)",
"iPad4,5" : "iPad mini Retina (GSM+CDMA)",
"iPad4,6" : "iPad mini Retina (China)",
"iPad4,7" : "iPad mini 3 (WiFi)",
"iPad4,8" : "iPad mini 3 (GSM+CDMA)",
"iPad4,9" : "iPad Mini 3 (China)",
"iPad5,1" : "iPad mini 4 (WiFi)",
"iPad5,2" : "4th Gen iPad mini (WiFi+Cellular)",
"iPad5,3" : "iPad Air 2 (WiFi)",
"iPad5,4" : "iPad Air 2 (Cellular)",
"iPad6,3" : "iPad Pro (9.7 inch, WiFi)",
"iPad6,4" : "iPad Pro (9.7 inch, WiFi+LTE)",
"iPad6,7" : "iPad Pro (12.9 inch, WiFi)",
"iPad6,8" : "iPad Pro (12.9 inch, WiFi+LTE)",
"iPad6,11" : "iPad (2017)",
"iPad6,12" : "iPad (2017)",
"iPad7,1" : "iPad Pro 2nd Gen (WiFi)",
"iPad7,2" : "iPad Pro 2nd Gen (WiFi+Cellular)",
"iPad7,3" : "iPad Pro 10.5-inch 2nd Gen",
"iPad7,4" : "iPad Pro 10.5-inch 2nd Gen",
"iPad7,5" : "iPad 6th Gen (WiFi)",
"iPad7,6" : "iPad 6th Gen (WiFi+Cellular)",
"iPad7,11" : "iPad 7th Gen 10.2-inch (WiFi)",
"iPad7,12" : "iPad 7th Gen 10.2-inch (WiFi+Cellular)",
"iPad8,1" : "iPad Pro 11 inch 3rd Gen (WiFi)",
"iPad8,2" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi)",
"iPad8,3" : "iPad Pro 11 inch 3rd Gen (WiFi+Cellular)",
"iPad8,4" : "iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)",
"iPad8,5" : "iPad Pro 12.9 inch 3rd Gen (WiFi)",
"iPad8,6" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)",
"iPad8,7" : "iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)",
"iPad8,8" : "iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)",
"iPad8,9" : "iPad Pro 11 inch 4th Gen (WiFi)",
"iPad8,10" : "iPad Pro 11 inch 4th Gen (WiFi+Cellular)",
"iPad8,11" : "iPad Pro 12.9 inch 4th Gen (WiFi)",
"iPad8,12" : "iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)",
"iPad11,1" : "iPad mini 5th Gen (WiFi)",
"iPad11,2" : "iPad mini 5th Gen",
"iPad11,3" : "iPad Air 3rd Gen (WiFi)",
"iPad11,4" : "iPad Air 3rd Gen",
"iPad11,6" : "iPad 8th Gen (WiFi)",
"iPad11,7" : "iPad 8th Gen (WiFi+Cellular)",
"iPad12,1" : "iPad 9th Gen (WiFi)",
"iPad12,2" : "iPad 9th Gen (WiFi+Cellular)",
"iPad14,1" : "iPad mini 6th Gen (WiFi)",
"iPad14,2" : "iPad mini 6th Gen (WiFi+Cellular)",
"iPad13,1" : "iPad Air 4th Gen (WiFi)",
"iPad13,2" : "iPad Air 4th Gen (WiFi+Cellular)",
"iPad13,4" : "iPad Pro 11 inch 5th Gen",
"iPad13,5" : "iPad Pro 11 inch 5th Gen",
"iPad13,6" : "iPad Pro 11 inch 5th Gen",
"iPad13,7" : "iPad Pro 11 inch 5th Gen",
"iPad13,8" : "iPad Pro 12.9 inch 5th Gen",
"iPad13,9" : "iPad Pro 12.9 inch 5th Gen",
"iPad13,10" : "iPad Pro 12.9 inch 5th Gen",
"iPad13,11" : "iPad Pro 12.9 inch 5th Gen",
"iPad13,16" : "iPad Air 5th Gen (WiFi)",
"iPad13,17" : "iPad Air 5th Gen (WiFi+Cellular)",
"iPad13,18" : "iPad 10th Gen",
"iPad13,19" : "iPad 10th Gen",
"iPad14,3" : "iPad Pro 11 inch 4th Gen",
"iPad14,4" : "iPad Pro 11 inch 4th Gen",
"iPad14,5" : "iPad Pro 12.9 inch 6th Gen",
"iPad14,6" : "iPad Pro 12.9 inch 6th Gen",
"Watch1,1" : "Apple Watch 38mm case",
"Watch1,2" : "Apple Watch 42mm case",
"Watch2,6" : "Apple Watch Series 1 38mm case",
"Watch2,7" : "Apple Watch Series 1 42mm case",
"Watch2,3" : "Apple Watch Series 2 38mm case",
"Watch2,4" : "Apple Watch Series 2 42mm case",
"Watch3,1" : "Apple Watch Series 3 38mm case (GPS+Cellular)",
"Watch3,2" : "Apple Watch Series 3 42mm case (GPS+Cellular)",
"Watch3,3" : "Apple Watch Series 3 38mm case (GPS)",
"Watch3,4" : "Apple Watch Series 3 42mm case (GPS)",
"Watch4,1" : "Apple Watch Series 4 40mm case (GPS)",
"Watch4,2" : "Apple Watch Series 4 44mm case (GPS)",
"Watch4,3" : "Apple Watch Series 4 40mm case (GPS+Cellular)",
"Watch4,4" : "Apple Watch Series 4 44mm case (GPS+Cellular)",
"Watch5,1" : "Apple Watch Series 5 40mm case (GPS)",
"Watch5,2" : "Apple Watch Series 5 44mm case (GPS)",
"Watch5,3" : "Apple Watch Series 5 40mm case (GPS+Cellular)",
"Watch5,4" : "Apple Watch Series 5 44mm case (GPS+Cellular)",
"Watch5,9" : "Apple Watch SE 40mm case (GPS)",
"Watch5,10" : "Apple Watch SE 44mm case (GPS)",
"Watch5,11" : "Apple Watch SE 40mm case (GPS+Cellular)",
"Watch5,12" : "Apple Watch SE 44mm case (GPS+Cellular)",
"Watch6,1" : "Apple Watch Series 6 40mm case (GPS)",
"Watch6,2" : "Apple Watch Series 6 44mm case (GPS)",
"Watch6,3" : "Apple Watch Series 6 40mm case (GPS+Cellular)",
"Watch6,4" : "Apple Watch Series 6 44mm case (GPS+Cellular)",
"Watch6,6" : "Apple Watch Series 7 41mm case (GPS)",
"Watch6,7" : "Apple Watch Series 7 45mm case (GPS)",
"Watch6,8" : "Apple Watch Series 7 41mm case (GPS+Cellular)",
"Watch6,9" : "Apple Watch Series 7 45mm case (GPS+Cellular)",
"Watch6,10" : "Apple Watch SE 40mm case (GPS)",
"Watch6,11" : "Apple Watch SE 44mm case (GPS)",
"Watch6,12" : "Apple Watch SE 40mm case (GPS+Cellular)",
"Watch6,13" : "Apple Watch SE 44mm case (GPS+Cellular)",
"Watch6,14" : "Apple Watch Series 8 41mm case (GPS)",
"Watch6,15" : "Apple Watch Series 8 45mm case (GPS)",
"Watch6,16" : "Apple Watch Series 8 41mm case (GPS+Cellular)",
"Watch6,17" : "Apple Watch Series 8 45mm case (GPS+Cellular)",
"Watch6,18" : "Apple Watch Ultra",
"Watch7,1" : "Apple Watch Series 9 41mm case (GPS)",
"Watch7,2" : "Apple Watch Series 9 45mm case (GPS)",
"Watch7,3" : "Apple Watch Series 9 41mm case (GPS+Cellular)",
"Watch7,4" : "Apple Watch Series 9 45mm case (GPS+Cellular)",
"Watch7,5" : "Apple Watch Ultra 2",
]

View File

@ -1,13 +0,0 @@
import Foundation
import SQLite
struct DataSeriesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("data_series")
}

View File

@ -1,98 +0,0 @@
import Foundation
import SQLite
import CoreLocation
typealias LocationSample = CLLocation
struct LocationSeriesDataTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("location_series_data")
/// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]`
let seriesIdentifier = Expression<Int>("series_identifier")
let timestamp = Expression<Double>("timestamp")
let longitude = Expression<Double>("longitude")
let latitude = Expression<Double>("latitude")
let altitude = Expression<Double>("altitude")
let speed = Expression<Double>("speed")
let course = Expression<Double>("course")
let horizontalAccuracy = Expression<Double>("horizontal_accuracy")
let verticalAccuracy = Expression<Double>("vertical_accuracy")
let speedAccuracy = Expression<Double>("speed_accuracy")
let courseAccuracy = Expression<Double>("course_accuracy")
let signalEnvironment = Expression<Double>("signal_environment")
func locationSamples(for seriesId: Int) throws -> [LocationSample] {
try database.prepare(table.filter(seriesIdentifier == seriesId)).map(location)
}
func locationSampleCount(for seriesId: Int) throws -> Int {
try database.scalar(table.filter(seriesIdentifier == seriesId).count)
}
func locationSamples(from start: Date, to end: Date) throws -> [LocationSample] {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.prepare(table.filter(timestamp >= startTime && timestamp <= endTime)).map(location)
}
func locationSampleCount(from start: Date, to end: Date) throws -> Int {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.scalar(table.filter(timestamp >= startTime && timestamp <= endTime).count)
}
func location(row: Row) -> LocationSample {
.init(
coordinate: .init(
latitude: row[latitude],
longitude: row[longitude]),
altitude: row[altitude],
horizontalAccuracy: row[horizontalAccuracy],
verticalAccuracy: row[horizontalAccuracy],
course: row[course],
courseAccuracy: row[courseAccuracy],
speed: row[speed],
speedAccuracy: row[speedAccuracy],
timestamp: .init(timeIntervalSinceReferenceDate: row[timestamp]),
sourceInfo: .init())
}
func create(references dataSeries: DataSeriesTable) throws {
try database.execute("CREATE TABLE location_series_data (series_identifier INTEGER NOT NULL REFERENCES data_series(hfd_key) DEFERRABLE INITIALLY DEFERRED, timestamp REAL NOT NULL, longitude REAL NOT NULL, latitude REAL NOT NULL, altitude REAL NOT NULL, speed REAL NOT NULL, course REAL NOT NULL, horizontal_accuracy REAL NOT NULL, vertical_accuracy REAL NOT NULL, speed_accuracy REAL NOT NULL, course_accuracy REAL NOT NULL, signal_environment INTEGER NOT NULL, PRIMARY KEY (series_identifier, timestamp)) WITHOUT ROWID")
}
func insert(_ sample: LocationSample, seriesId: Int) throws {
try database.run(table.insert(
seriesIdentifier <- seriesId,
timestamp <- sample.timestamp.timeIntervalSinceReferenceDate,
longitude <- sample.coordinate.longitude,
latitude <- sample.coordinate.latitude,
altitude <- sample.altitude,
speed <- sample.speed,
course <- sample.course,
horizontalAccuracy <- sample.horizontalAccuracy,
horizontalAccuracy <- sample.verticalAccuracy,
speedAccuracy <- sample.speedAccuracy,
courseAccuracy <- sample.courseAccuracy,
signalEnvironment <- 1
))
}
}

View File

@ -1,44 +0,0 @@
import Foundation
import SQLite
struct MetadataKeysTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("metadata_keys")
let rowId = Expression<Int>("ROWID")
let key = Expression<String>("key")
func key(for keyId: Int, in database: Connection) throws -> String {
try database.pluck(table.filter(rowId == keyId)).map { $0[key] }!
}
func all() throws -> [Int : Metadata.Key] {
try database.prepare(table).reduce(into: [:]) { dict, row in
dict[row[rowId]] = .init(rawValue: row[key])
}
}
func create() throws {
//try database.execute("CREATE TABLE metadata_keys (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE)")
try database.run(table.create { table in
table.column(rowId, primaryKey: .autoincrement)
table.column(key, unique: true)
})
}
func hasKey(_ key: Metadata.Key) throws -> Int? {
try database.pluck(table.filter(self.key == key.rawValue)).map { $0[rowId] }
}
func insert(key: Metadata.Key) throws -> Int {
Int(try database.run(table.insert(self.key <- key.rawValue)))
}
}

View File

@ -1,42 +0,0 @@
import Foundation
import SQLite
struct MetadataTables {
private let database: Connection
let values: MetadataValuesTable
let keys: MetadataKeysTable
init(database: Connection) {
self.database = database
self.keys = .init(database: database)
self.values = .init(database: database)
}
func create() throws {
try values.create()
try keys.create()
}
func metadata(for workoutId: Int) throws -> [Metadata.Key : Metadata.Value] {
// Keys: rowId -> String
let selection = values.table
.select(values.table[*], keys.table[keys.key])
.filter(values.objectId == workoutId)
.join(.leftOuter, keys.table, on: values.table[values.keyId] == keys.table[keys.rowId])
return try database.prepare(selection).reduce(into: [:]) { dict, row in
let key = Metadata.Key(rawValue: row[keys.key])
let value = values.from(row: row)
dict[key] = value
}
}
func insert(_ value: Metadata.Value, for key: Metadata.Key, of workoutId: Int) throws {
let keyId = try keys.hasKey(key) ?? keys.insert(key: key)
try values.insert(value, of: workoutId, for: keyId)
}
}

View File

@ -1,126 +0,0 @@
import Foundation
import SQLite
struct MetadataValuesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("metadata_values")
let rowId = Expression<Int>("ROW_ID")
let keyId = Expression<Int?>("key_id")
let objectId = Expression<Int?>("object_id")
let valueType = Expression<Int>("value_type")
let stringValue = Expression<String?>("string_value")
let numericalValue = Expression<Double?>("numerical_value")
let dateValue = Expression<Double?>("date_value")
let dataValue = Expression<Data?>("data_value")
func all() throws -> [Metadata.Value] {
try database.prepare(table).map(from)
}
func metadata(for workoutId: Int) throws -> [Metadata.Value] {
try database.prepare(table.filter(objectId == workoutId)).map(from)
}
func metadata(for workoutId: Int) throws -> [(keyId: Int, value: Metadata.Value)] {
try database.prepare(table.filter(objectId == workoutId)).compactMap { row in
guard let keyId = row[keyId] else {
print("Found 'key_id == NULL' for metadata value of workout \(workoutId)")
return nil
}
return (keyId, from(row: row))
}
}
func from(row: Row) -> Metadata.Value {
let valueType = Metadata.Value.ValueType(rawValue: row[valueType])!
switch valueType {
case .string:
return .string(value: row[stringValue]!)
case .number:
return .number(value: row[numericalValue]!)
case .date:
return .date(value: .init(timeIntervalSinceReferenceDate: row[dateValue]!))
case .numerical:
return .numerical(value: row[numericalValue]!, unit: row[stringValue]!)
case .data:
return .data(value: row[dataValue]!)
}
}
func create() throws {
//try database.execute("CREATE TABLE metadata_values (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key_id INTEGER, object_id INTEGER, value_type INTEGER NOT NULL DEFAULT 0, string_value TEXT, numerical_value REAL, date_value REAL, data_value BLOB)")
try database.run(table.create { table in
table.column(rowId, primaryKey: .autoincrement)
table.column(keyId)
table.column(objectId)
table.column(valueType, defaultValue: 0)
table.column(stringValue)
table.column(numericalValue)
table.column(dateValue)
table.column(dataValue)
})
}
func insert(_ element: Metadata.Value, of workoutId: Int, for keyId: Int) throws {
try database.run(table.insert(
self.keyId <- keyId,
objectId <- workoutId,
valueType <- element.valueType.rawValue,
stringValue <- element.stringValue,
numericalValue <- element.numericalValue,
dateValue <- element.dateValue,
dataValue <- element.dataValue
))
}
}
private extension Metadata.Value {
var stringValue: String? {
if case let .string(value) = self {
return value
}
if case let .numerical(_, unit) = self {
return unit
}
return nil
}
var numericalValue: Double? {
if case let .number(value) = self {
return value
}
if case let .numerical(value: value, unit: _) = self {
return value
}
return nil
}
var dateValue: Double? {
if case let .date(value: date) = self {
return date.timeIntervalSinceReferenceDate
}
return nil
}
var dataValue: Data? {
if case let .data(data) = self {
return data
}
return nil
}
}

View File

@ -1,37 +0,0 @@
import Foundation
import SQLite
struct ObjectsTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
func create(referencing dataProvenances: DataProvenancesTable) throws {
try database.execute("CREATE TABLE objects (data_id INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE, provenance INTEGER NOT NULL REFERENCES data_provenances (ROWID) ON DELETE CASCADE, type INTEGER, creation_date REAL)")
}
let table = Table("objects")
let dataId = Expression<Int>("data_id")
let uuid = Expression<Data?>("uuid")
let provenance = Expression<Int>("provenance")
let type = Expression<Int?>("type")
let creationDate = Expression<Double?>("creation_date")
func object(for dataId: Int) throws -> (uuid: UUID, provenance: Int, type: Int, creationDate: Date)? {
try database.pluck(table.filter(self.dataId == dataId)).map { row in
let uuid = row[uuid]!.asUUID()!
let provenance = row[provenance]
let type = row[type]!
let creationDate = Date(timeIntervalSinceReferenceDate: row[creationDate]!)
return (uuid, provenance, type, creationDate)
}
}
}

View File

@ -1,32 +0,0 @@
import Foundation
import SQLite
struct QuantitySamplesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
func create(referencing unitStrings: UnitStringsTable) throws {
try database.execute("CREATE TABLE quantity_samples (data_id INTEGER PRIMARY KEY, quantity REAL, original_quantity REAL, original_unit INTEGER REFERENCES unit_strings (ROWID) ON DELETE NO ACTION)")
}
let table = Table("quantity_samples")
let dataId = Expression<Int>("data_id")
let quantity = Expression<Double?>("quantity")
let originalQuantity = Expression<Double?>("original_quantity")
/// References `ROWID` on table `unit_strings`
let originalUnit = Expression<Int?>("original_unit")
func quantity(for id: Int, in database: Database) throws -> (quantity: Double?, original: Double?, unit: Int?) {
try database.prepare(table.filter(dataId == id).limit(1)).map {
(quantity: $0[quantity], original: $0[originalQuantity], unit: $0[originalUnit])
}.first ?? (nil, nil, nil)
}
}

View File

@ -1,96 +0,0 @@
import Foundation
import SQLite
struct SamplesTable {
private let database: Connection
private let quantitySamples: QuantitySamplesTable
private let objects: ObjectsTable
private let dataProvenances: DataProvenancesTable
private let unitStrings: UnitStringsTable
init(database: Connection) {
self.database = database
self.quantitySamples = .init(database: database)
self.objects = .init(database: database)
self.dataProvenances = .init(database: database)
self.unitStrings = .init(database: database)
}
func create() throws {
try database.execute("CREATE TABLE samples (data_id INTEGER PRIMARY KEY, start_date REAL, end_date REAL, data_type INTEGER)")
}
func createAll() throws {
try create()
try unitStrings.create()
try quantitySamples.create(referencing: unitStrings)
try dataProvenances.create()
try objects.create(referencing: dataProvenances)
}
private let table = Table("samples")
private let dataId = Expression<Int>("data_id")
// NOTE: Technically optional
private let startDate = Expression<Double>("start_date")
// NOTE: Technically optional
private let endDate = Expression<Double>("end_date")
private let dataType = Expression<Int>("data_type")
func samples(from start: Date, to end: Date) throws -> [Sample] {
let start = start.timeIntervalSinceReferenceDate
let end = end.timeIntervalSinceReferenceDate
// Samples: data_id, start_date, end_date, data_type
// JOIN quantity_samples on samples.data_id == quantity_samples.data_id
// quantity_samples: quantity, original_quantity, original_unit
// JOIN objects on samples.data_id == objects.data_id
// objects: data_id, uuid, provenance, type, creation_date
// JOIN data_provenances on objects.provenance == data_provenances.ROWID
// SELECT tz_name FROM data_provenances
let selection = table
.select(table[*],
quantitySamples.table[*],
dataProvenances.table[dataProvenances.tzName],
unitStrings.table[unitStrings.unitString])
.filter(startDate >= start && endDate <= end)
.join(.leftOuter, quantitySamples.table, on: table[dataId] == quantitySamples.table[quantitySamples.dataId])
.join(.leftOuter, objects.table, on: table[dataId] == objects.table[objects.dataId])
.join(.leftOuter, dataProvenances.table, on: objects.table[objects.provenance] == dataProvenances.table[dataProvenances.rowId])
.join(.leftOuter, unitStrings.table, on: quantitySamples.table[quantitySamples.originalUnit] == unitStrings.table[unitStrings.rowId])
return try database.prepare(selection).map { row in
let startDate = Date(timeIntervalSinceReferenceDate: row[startDate])
let endDate = Date(timeIntervalSinceReferenceDate: row[endDate])
let dataType = Sample.DataType(rawValue: row[dataType])
let quantity = row[quantitySamples.quantity]
let original = row[quantitySamples.originalQuantity]
let unit = row[unitStrings.unitString]
let timeZone = row[dataProvenances.tzName].nonEmpty
return .init(
startDate: startDate,
endDate: endDate,
dataType: dataType,
quantity: quantity,
originalQuantity: original,
originalUnit: unit,
timeZoneName: timeZone)
}
}
func sampleCount(from start: Date, to end: Date) throws -> Int {
let start = start.timeIntervalSinceReferenceDate
let end = end.timeIntervalSinceReferenceDate
return try database.scalar(table.filter(startDate >= start && endDate <= end).count)
}
}

View File

@ -1,27 +0,0 @@
import Foundation
import SQLite
struct UnitStringsTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
func create() throws {
try database.execute("CREATE TABLE unit_strings (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, unit_string TEXT UNIQUE)")
}
let table = Table("unit_strings")
let rowId = Expression<Int>("ROWID")
let unitString = Expression<String?>("unit_string")
func unit(for id: Int) throws -> String? {
try database.pluck(table.filter(rowId == id).limit(1)).map { row in
row[unitString]
}
}
}

View File

@ -1,119 +0,0 @@
import Foundation
import SQLite
import HealthKit
struct WorkoutActivitiesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("workout_activities")
let rowId = Expression<Int>("ROWID")
let uuid = Expression<Data>("uuid")
let ownerId = Expression<Int>("owner_id")
let isPrimaryActivity = Expression<Bool>("is_primary_activity")
let activityType = Expression<Int>("activity_type")
let locationType = Expression<Int>("location_type")
let swimmingLocationType = Expression<Int>("swimming_location_type")
let lapLength = Expression<Data?>("lap_length")
let startDate = Expression<Double>("start_date")
let endDate = Expression<Double>("end_date")
let duration = Expression<Double>("duration")
let metadata = Expression<Data?>("metadata")
func activities() throws -> [HKWorkoutActivity] {
try database.prepare(table).map(activity)
}
func activity(from row: Row) throws -> HKWorkoutActivity {
let configuration = HKWorkoutConfiguration()
configuration.lapLength = try row[lapLength].map(WorkoutActivitiesTable.lapLength)
configuration.activityType = .init(rawValue: UInt(row[activityType]))!
configuration.locationType = .init(rawValue: row[locationType])!
configuration.swimmingLocationType = .init(rawValue: row[swimmingLocationType])!
let start = Date(timeIntervalSinceReferenceDate: row[startDate])
let end = Date(timeIntervalSinceReferenceDate: row[endDate])
let uuid = row[uuid].uuidString
var metadata: [String : Any] = [ : ]
metadata[HKMetadataKeyExternalUUID] = uuid
// duration: row[columnDuration]
// isPrimaryActivity: row[columnIsPrimaryActivity]
// metadata: row[columnMetadata]
// TODO: Decode activity metadata
return .init(
workoutConfiguration: configuration,
start: start,
end: end,
metadata: metadata)
}
func activities(for workoutId: Int) throws -> [HKWorkoutActivity] {
try database.prepare(table.filter(ownerId == workoutId)).map(activity)
}
func create(referencing workouts: WorkoutsTable) throws {
try database.execute("CREATE TABLE workout_activities (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE NOT NULL, owner_id INTEGER NOT NULL REFERENCES workouts(data_id) ON DELETE CASCADE, is_primary_activity INTEGER NOT NULL, activity_type INTEGER NOT NULL, location_type INTEGER NOT NULL, swimming_location_type INTEGER NOT NULL, lap_length BLOB, start_date REAL NOT NULL, end_date REAL NOT NULL, duration REAL NOT NULL, metadata BLOB)")
/*
try database.run(table.create { t in
t.column(rowId, primaryKey: .autoincrement)
t.column(uuid, unique: true)
t.column(ownerId, references: workouts.table, workouts.dataId) // TODO: ON DELETE CASCADE
t.column(isPrimaryActivity)
t.column(activityType)
t.column(locationType)
t.column(swimmingLocationType)
t.column(lapLength)
t.column(startDate)
t.column(endDate)
t.column(duration)
t.column(metadata)
})
*/
}
func insert(_ element: HKWorkoutActivity, isPrimaryActivity: Bool, dataId: Int) throws {
try database.run(table.insert(
uuid <- (element.externalUUID ?? element.uuid).uuidString.data(using: .utf8)!,
ownerId <- dataId,
self.isPrimaryActivity <- isPrimaryActivity, // Seems to always be 1
activityType <- Int(element.workoutConfiguration.activityType.rawValue),
locationType <- element.workoutConfiguration.locationType.rawValue,
swimmingLocationType <- element.workoutConfiguration.swimmingLocationType.rawValue,
lapLength <- try WorkoutActivitiesTable.lapLengthData(lapLength: element.workoutConfiguration.lapLength),
startDate <- element.startDate.timeIntervalSinceReferenceDate,
endDate <- element.endDate?.timeIntervalSinceReferenceDate ?? element.startDate.addingTimeInterval(element.duration).timeIntervalSinceReferenceDate,
duration <- element.duration,
metadata <- nil)
)
}
}
private extension WorkoutActivitiesTable {
static func lapLengthData(lapLength: HKQuantity?) throws -> Data? {
try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
}
static func lapLength(from data: Data) throws -> HKQuantity? {
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
}
}

View File

@ -1,174 +0,0 @@
import Foundation
import SQLite
import HealthKit
struct WorkoutEventsTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("workout_events")
// INTEGER PRIMARY KEY AUTOINCREMENT
let rowId = Expression<Int>("ROWID")
// owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE
let ownerId = Expression<Int>("owner_id")
// date REAL NOT NULL
let date = Expression<Double>("date")
// type INTEGER NOT NULL
let type = Expression<Int>("type")
// duration REAL NOT NULL
let duration = Expression<Double>("duration")
// metadata BLOB
let metadata = Expression<Data?>("metadata")
// session_uuid BLOB
let sessionUUID = Expression<Data?>("session_uuid")
// error BLOB
let error = Expression<Data?>("error")
func events(in database: Connection) throws -> [HKWorkoutEvent] {
try database.prepare(table).map(event)
}
func events(for workoutId: Int, in database: Connection) throws -> [HKWorkoutEvent] {
try database.prepare(table.filter(ownerId == workoutId)).map(event)
}
private func event(from row: Row) -> HKWorkoutEvent {
let start = Date(timeIntervalSinceReferenceDate: row[date])
let interval = DateInterval(start: start, duration: row[duration])
let metadata = metadata(row[metadata])
let type = HKWorkoutEventType(rawValue: row[type])!
// let sessionUUID = row[sessionUUID]
// let error = row[rrror]
return .init(type: type, dateInterval: interval, metadata: metadata)
}
private func metadata(_ data: Data?) -> [String : Any] {
guard let data else {
return [:]
}
return WorkoutEventsTable.decode(metadata: data)
}
func create(referencing workouts: WorkoutsTable) throws {
// try database.execute("CREATE TABLE workout_events (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE, date REAL NOT NULL, type INTEGER NOT NULL, duration REAL NOT NULL, metadata BLOB, session_uuid BLOB, error BLOB)")
try database.run(table.create { t in
t.column(rowId, primaryKey: .autoincrement)
t.column(ownerId, references: workouts.table, workouts.dataId)
t.column(date)
t.column(type)
t.column(duration)
t.column(metadata)
t.column(sessionUUID)
t.column(error)
})
}
func insert(_ element: HKWorkoutEvent, dataId: Int) throws {
try database.run(table.insert(
ownerId <- dataId,
date <- element.dateInterval.start.timeIntervalSinceReferenceDate,
type <- element.type.rawValue,
duration <- element.dateInterval.duration,
metadata <- WorkoutEventsTable.encode(metadata: element.metadata ?? [:]))
// SessionUUID <- element.sessionUUID
// Error <- element.error)
)
}
}
extension WorkoutEventsTable {
static func decode(metadata data: Data) -> [String : Any] {
let metadata: WorkoutEventMetadata
do {
metadata = try WorkoutEventMetadata(serializedData: data)
} catch {
print("Failed to decode event metadata: \(error)")
print(data.hex)
return [:]
}
return metadata.elements.reduce(into: [:]) { dict, element in
guard let value = element.value else {
print("No value for metadata element \(element)")
print(data.hex)
return
}
dict[element.key] = value
}
}
static func encode(metadata: [String : Any]) -> Data? {
let wrapper = WorkoutEventMetadata.with {
$0.elements = metadata.compactMap { .from(key: $0.key, value: $0.value) }
}
guard !wrapper.elements.isEmpty else {
return nil
}
do {
return try wrapper.serializedData()
} catch {
print("Failed to encode event metadata: \(error)")
return nil
}
}
}
private extension WorkoutEventMetadata.Element {
var value: Any? {
if hasUnsignedValue {
return unsignedValue
}
if hasQuantity {
return HKQuantity(unit: .init(from: quantity.unit), doubleValue: quantity.value)
}
return UInt(0)
}
static func from(key: String, value: Any) -> Self? {
if let value = value as? UInt64 {
return .with {
$0.key = key
$0.unsignedValue = UInt64(value)
}
}
guard let value = value as? HKQuantity else {
print("Unknown value type for metadata key \(key): \(value)")
return nil
}
let number: Double
let unit: String
if value.is(compatibleWith: .meter()) {
number = value.doubleValue(for: .meter())
unit = "m"
} else if value.is(compatibleWith: .second()) {
number = value.doubleValue(for: .second())
unit = "s"
} else {
print("Unhandled quantity type for metadata key \(key): \(value)")
return nil
}
return .with { el in
el.key = key
el.quantity = .with {
$0.value = number
$0.unit = unit
}
}
}
}

View File

@ -1,102 +0,0 @@
import Foundation
import SQLite
import HealthKit
struct WorkoutsTable {
private let database: Connection
let events: WorkoutEventsTable
let activities: WorkoutActivitiesTable
let metadata: MetadataTables
init(database: Connection) {
self.database = database
self.events = .init(database: database)
self.activities = .init(database: database)
self.metadata = .init(database: database)
}
let table = Table("workouts")
// INTEGER PRIMARY KEY AUTOINCREMENT
let dataId = Expression<Int>("data_id")
// REAL
let totalDistance = Expression<Double?>("total_distance")
// INTEGER
let goalType = Expression<Int?>("goal_type")
// REAL
let goal = Expression<Double?>("goal")
// INTEGER
let condenserVersion = Expression<Int?>("condenser_version")
// REAL
let condenserDate = Expression<Double?>("condenser_date")
func workouts() throws -> [Workout] {
return try database.prepare(table).map { row in
let id = row[dataId]
let events = try events.events(for: id, in: database)
let activities = try activities.activities(for: id)
let metadata = try metadata.metadata(for: id)
return .init(
id: id,
totalDistance: row[totalDistance],
goalType: row[goalType],
goal: row[goal],
condenserVersion: row[condenserVersion],
condenserDate: row[condenserDate].map { Date.init(timeIntervalSinceReferenceDate: $0) },
events: events,
activities: activities,
metadata: metadata)
}
}
func create() throws {
try database.run(table.create { t in
t.column(dataId, primaryKey: .autoincrement)
t.column(totalDistance)
t.column(goalType)
t.column(goal)
t.column(condenserVersion)
t.column(condenserDate)
})
// try database.execute("CREATE TABLE workouts (data_id INTEGER PRIMARY KEY AUTOINCREMENT, total_distance REAL, goal_type INTEGER, goal REAL, condenser_version INTEGER, condenser_date REAL)")
}
func createAll() throws {
try create()
try events.create(referencing: self)
try activities.create(referencing: self)
}
func insert(_ element: Workout) throws {
let rowid = try database.run(table.insert(
totalDistance <- element.totalDistance,
goalType <- element.goal?.goalType,
goal <- element.goal?.gaol,
condenserVersion <- element.condenserVersion,
condenserDate <- element.condenserDate?.timeIntervalSinceReferenceDate)
)
let dataId = Int(rowid)
for event in element.events {
try events.insert(event, dataId: dataId)
}
for activity in element.activities {
try activities.insert(activity, isPrimaryActivity: true, dataId: dataId)
}
for (key, value) in element.metadata {
try metadata.insert(value, for: key, of: dataId)
}
}
}

View File

@ -1,80 +0,0 @@
import Foundation
import Collections
import HealthKit
private let df: DateFormatter = {
let df = DateFormatter()
df.timeZone = .current
df.dateStyle = .short
df.timeStyle = .short
return df
}()
struct Workout {
let id: Int
/// The distance in km (?)
let totalDistance: Double?
let goal: Goal?
let condenserVersion: Int?
let condenserDate: Date?
let events: [HKWorkoutEvent]
let activities: [HKWorkoutActivity]
let metadata: OrderedDictionary<Metadata.Key, Metadata.Value>
var firstActivityDate: Date? {
activities.map { $0.startDate }.min()
}
var firstEventDate: Date? {
events.map { $0.dateInterval.start }.min()
}
var firstAvailableDate: Date? {
[condenserDate, firstEventDate, firstActivityDate].compactMap { $0 }.min()
}
var dateString: String {
guard let firstAvailableDate else {
return "No date"
}
return df.string(from: firstAvailableDate)
}
var typeString: String {
activities.first?.workoutConfiguration.activityType.description ?? "Unknown activity"
}
init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [HKWorkoutEvent] = [], activities: [HKWorkoutActivity] = [], metadata: [Metadata.Key : Metadata.Value] = [:]) {
self.id = id
self.totalDistance = totalDistance
self.goal = .init(goalType: goalType, goal: goal)
self.condenserVersion = condenserVersion
self.condenserDate = condenserDate
self.events = events
self.activities = activities
self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values)
}
}
extension Workout: Identifiable { }
extension Workout: Equatable {
static func == (lhs: Workout, rhs: Workout) -> Bool {
lhs.id == rhs.id
}
}
extension Workout: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@ -1,6 +1,7 @@
import Foundation
import SQLite
import HealthKit
import HKDatabase
extension HealthDatabase {

View File

@ -1,9 +1,9 @@
import Foundation
import CoreLocation
extension LocationSample {
extension CLLocation {
static let mock: LocationSample = .init(
static let mock: CLLocation = .init(
coordinate: .init(latitude: 52.27124117, longitude: 10.53865853),
altitude: 2340, // m
horizontalAccuracy: 3.5, // m

View File

@ -1,34 +1,37 @@
import Foundation
import HKDatabase
import HealthKitExtensions
import HealthKit
extension Metadata {
static let mock1: [Key : Value] = [
.indoorWorkout : .number(value: 0.0),
.elevationAscended : .numerical(value: 100564.0, unit: "cm"),
.timeZone : .string(value: "Europe/Berlin"),
.weatherHumidity : .numerical(value: 84.0, unit: "%"),
.weatherTemperature : .numerical(value: 20.8219999999961, unit: "degF"),
.averageMETs : .numerical(value: 4.12470993502559, unit: "kcal/hr·kg"),
.privateWeatherCondition : .number(value: 4.0),
.privateWorkoutActivityMoveMode : .number(value: 1.0),
.privateWorkoutMaxGroundElevation : .numerical(value: 3188.78609748806, unit: "m"),
.privateWorkoutWeatherSourceName : .string(value: "Apple Weather"),
.privateWorkoutHeartRateZones : .data(value: .init(hex: "62706c6973743030a5010a121a22d402030405060708095f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657810052300000000000000002340604000000000001000d40b0c0d0e060f10115f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e6465782340604000000000002340620000000000001001d413141516061718195f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234062000000000000234063c000000000001002d41b1c1d1e061f20215f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234063c000000000002340658000000000001003d423242526062728295f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657823406580000000000023406740000000000010040008000e0017002c004000540069006b0074007d007f0088009d00b100c500da00e300ec00ee00f7010c0120013401490152015b015d0166017b018f01a301b801c101ca01cc01d501ea01fe02120227023002390000000000000201000000000000002a0000000000000000000000000000023b")!),
.privateWorkoutMinHeartRate : .numerical(value: 1.05, unit: "count/s"),
.privateMetricPlatterStatistics : .string(value: """
static let mock1: [String : Any] = [
HKMetadataKeyIndoorWorkout : NSNumber(0.0),
HKMetadataKeyElevationAscended : HKQuantity(unit: .meterUnit(with: .centi), doubleValue: 100564.0),
HKMetadataKeyTimeZone : "Europe/Berlin",
HKMetadataKeyWeatherHumidity : HKQuantity(unit: .percent(), doubleValue: 84.0),
HKMetadataKeyWeatherTemperature : HKQuantity(unit: .degreeCelsius(), doubleValue: 20.8219999999961),
HKMetadataKeyAverageMETs : HKQuantity(unit: .init(from: "kcal/hr*kg"), doubleValue: 4.12470993502559),
"_HKPrivateWeatherCondition" : NSNumber(4.0),
"_HKPrivateWorkoutActivityMoveMode" : NSNumber(1.0),
"_HKPrivateWorkoutMaxGroundElevation" : HKQuantity(unit: .meter(), doubleValue: 3188.78609748806),
"_HKPrivateWorkoutWeatherSourceName" : "Apple Weather",
"_HKPrivateWorkoutHeartRateZones" : Data(hex: "62706c6973743030a5010a121a22d402030405060708095f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657810052300000000000000002340604000000000001000d40b0c0d0e060f10115f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e6465782340604000000000002340620000000000001001d413141516061718195f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234062000000000000234063c000000000001002d41b1c1d1e061f20215f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e646578234063c000000000002340658000000000001003d423242526062728295f1012636f6e66696775726174696f6e436f756e745f10116c6f776572446973706c6179426f756e645f10117570706572446973706c6179426f756e645f1012636f6e66696775726174696f6e496e64657823406580000000000023406740000000000010040008000e0017002c004000540069006b0074007d007f0088009d00b100c500da00e300ec00ee00f7010c0120013401490152015b015d0166017b018f01a301b801c101ca01cc01d501ea01fe02120227023002390000000000000201000000000000002a0000000000000000000000000000023b")!,
"_HKPrivateWorkoutMinHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 1.05),
"_HKPrivateMetadataKeyMetricPlatterStatistics" : """
{"currentMetricPlatterType":"standard","currentMetricPlatterElapsedTime":17991.661971926689,"metricPlatterAccumulatedTime":{"standard":5039.4511208534241,"activityRings":31.905926942825317,"elevation":12920.30492413044}}
"""),
.privateWorkoutWasInDaytime : .number(value: 1.0),
.privateWorkoutWeatherLocationCoordinatesLatitude : .number(value: 47.0850386887433),
.privateWorkoutMinGroundElevation : .numerical(value: 2147.15929389872, unit: "m"),
.privateWorkoutConfiguration : .data(value: """
""",
"_HKPrivateWorkoutWasInDaytime" : NSNumber(1.0),
"_HKPrivateWorkoutWeatherLocationCoordinatesLatitude" : NSNumber(47.0850386887433),
"_HKPrivateWorkoutMinGroundElevation" : HKQuantity(unit: .meter(), doubleValue: 2147.15929389872),
"_HKPrivateWorkoutConfiguration" : """
{"strings":{},"type":2,"data":"eyJ0eXBlIjoxLCJnb2FsIjoiWW5Cc2FYTjBNRERVQVFJREJBVXNMUzVZSkc5aWFtVmpkSE5ZSkhabGNuTnBiMjVaSkdGeVkyaHBkbVZ5VkNSMGIzQ29CZ2NRRmhvYkpDaFZKRzUxYkd6VUNBa0tDd3dORGc5ZkVCMU9URk5sYzNOcGIyNUJZM1JwZG1sMGVVZHZZV3hSZFdGdWRHbDBlVjhRSjA1TVUyVnpjMmx2YmtGamRHbDJhWFI1UjI5aGJFZHZZV3hVZVhCbFNXUmxiblJwWm1sbGNsWWtZMnhoYzNOZkVCcE9URk5sYzNOcGIyNUJZM1JwZG1sMGVVZHZZV3hXWVd4MVpZQUNFQUtBQnlOQTAxWUFBQUFBQU5NUkVnb1RGQlZZVm1Gc2RXVkxaWGxYVlc1cGRFdGxlU05BMDFZQUFBQUFBSUFEZ0FiU0NoY1lHVjhRRDBoTFZXNXBkRk4wY21sdVowdGxlWUFGZ0FSUmM5SWNIUjRqV0NSamJHRnpjMlZ6V2lSamJHRnpjMjVoYldXa0h5QWhJbHBJUzFScGJXVlZibWwwV2toTFFtRnpaVlZ1YVhSV1NFdFZibWwwV0U1VFQySnFaV04wV2toTFZHbHRaVlZ1YVhUU0hCMGxKNkltSWxwSVMxRjFZVzUwYVhSNVdraExVWFZoYm5ScGRIblNIQjBwSzZJcUlsOFFGVTVNVTJWemMybHZia0ZqZEdsMmFYUjVSMjloYkY4UUZVNU1VMlZ6YzJsdmJrRmpkR2wyYVhSNVIyOWhiQklBQVlhZ1h4QVBUbE5MWlhsbFpFRnlZMmhwZG1WeTBTOHdWSEp2YjNTQUFRQUlBQkVBR2dBakFDMEFNZ0E3QUVFQVNnQnFBSlFBbXdDNEFMb0F2QUMrQU1jQXpnRFhBTjhBNkFEcUFPd0E4UUVEQVFVQkJ3RUpBUTRCRndFaUFTY0JNZ0U5QVVRQlRRRllBVjBCWUFGckFYWUJld0YrQVpZQnJnR3pBY1VCeUFITkFBQUFBQUFBQWdFQUFBQUFBQUFBTVFBQUFBQUFBQUFBQUFBQUFBQUFBYzg9IiwidXVpZCI6IjQxQTAzQkFCLTlDMjktNDhGMC1CRTZELTFFOEJDREZCMjVEOSIsIm9jY3VycmVuY2UiOnsiY291bnQiOjAsImNyZWF0aW9uRGF0ZSI6Njg0NjY0MzE2LjkyODk5ODM1LCJtb2RpZmljYXRpb25EYXRlIjo3MDIxMDc0MDMuOTU0MjE2OTYsImNvdW50TW9kaWZpY2F0aW9uRGF0ZSI6Njg0NjY0MzE2LjkyODk5ODM1fSwiYWN0aXZpdHlUeXBlIjoiWW5Cc2FYTjBNRERVQVFJREJBVWtKU1pZSkc5aWFtVmpkSE5ZSkhabGNuTnBiMjVaSkdGeVkyaHBkbVZ5VkNSMGIzQ2xCZ2NVR2lCVkpHNTFiR3pXQ0FrS0N3d05EZzhPRVJJVFh4QWZSa2xWU1ZkdmNtdHZkWFJCWTNScGRtbDBlVlI1Y0dWSmMwbHVaRzl2Y2w4UUgwWkpWVWxYYjNKcmIzVjBRV04wYVhacGRIbFVlWEJsVFdWMFlXUmhkR0ZmRUNkR1NWVkpWMjl5YTI5MWRFRmpkR2wyYVhSNVZIbHdaVkJoY25SUFprMTFiSFJwYzNCdmNuUldKR05zWVhOelh4QWhSa2xWU1ZkdmNtdHZkWFJCWTNScGRtbDBlVlI1Y0dWSlpHVnVkR2xtYVdWeVh4QW9Ua3hUWlhOemFXOXVRWFY0YVd4cFlYSjVRV04wYVhacGRIbFVlWEJsU1dSbGJuUnBabWxsY2dpQUFnaUFCQkFZRUFEVEZRc1dGeGdaV2s1VExtOWlhbVZqZEhOWFRsTXVhMlY1YzZDQUE2RFNHeHdkSGxna1kyeGhjM05sYzFva1kyeGhjM051WVcxbG9oNGZYRTVUUkdsamRHbHZibUZ5ZVZoT1UwOWlhbVZqZE5JYkhDRWpvaUlmWHhBWFJrbFZTVmR2Y210dmRYUkJZM1JwZG1sMGVWUjVjR1ZmRUJkR1NWVkpWMjl5YTI5MWRFRmpkR2wyYVhSNVZIbHdaUklBQVlhZ1h4QVBUbE5MWlhsbFpFRnlZMmhwZG1WeTBTY29WSEp2YjNTQUFRQUlBQkVBR2dBakFDMEFNZ0E0QUQ0QVN3QnRBSThBdVFEQUFPUUJEd0VRQVJJQkV3RVZBUmNCR1FFZ0FTc0JNd0UwQVRZQk53RThBVVVCVUFGVEFXQUJhUUZ1QVhFQml3R2xBYW9CdkFHXC9BY1FBQUFBQUFBQUNBUUFBQUFBQUFBQXBBQUFBQUFBQUFBQUFBQUFBQUFBQnhnPT0ifQ==","objectModificationDate":702107514.08573401,"version":1,"numbers":{"configuration_type":1,"goal_type":2},"uuid":"41A03BAB-9C29-48F0-BE6D-1E8BCDFB25D9","objectState":0}
""".data(using: .utf8)!),
.privateWorkoutWeatherLocationCoordinatesLongitude : .number(value: 11.1684857327149),
.privateWorkoutHeartRateZonesConfigurationType : .number(value: 0.0),
.privateWorkoutAverageHeartRate : .numerical(value: 1.83192725788713, unit: "count/s"),
.privateWorkoutExtendedMode : .number(value: 0.0),
.privateWorkoutMaxHeartRate : .numerical(value: 2.58333333333333, unit: "count/s"),
.privateWorkoutElapsedTimeInHeartRateZones : .data(value: .init(hex: "62706c6973743030d30102030405065131513251302340ad88000014000023407c37d7626000002340d637c27a270000080f1113151e270000000000000101000000000000000700000000000000000000000000000030")!)
""".data(using: .utf8)!,
"_HKPrivateWorkoutWeatherLocationCoordinatesLongitude" : NSNumber(11.1684857327149),
"_HKPrivateWorkoutHeartRateZonesConfigurationType" : NSNumber(0.0),
"_HKPrivateWorkoutAverageHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 1.83192725788713),
"_HKPrivateWorkoutExtendedMode" : NSNumber(0.0),
"_HKPrivateWorkoutMaxHeartRate" : HKQuantity(unit: .count().unitDivided(by: .second()), doubleValue: 2.58333333333333),
"_HKPrivateWorkoutElapsedTimeInHeartRateZones" : Data(hex: "62706c6973743030d30102030405065131513251302340ad88000014000023407c37d7626000002340d637c27a270000080f1113151e270000000000000101000000000000000700000000000000000000000000000030")!
]
}

View File

@ -1,5 +1,7 @@
import Foundation
import HealthKit
import HKDatabase
import HealthKitExtensions
extension Workout {

View File

@ -8,15 +8,36 @@ extension HKWorkoutEvent {
.init(type: .init(rawValue: 7)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
duration: 1114.56374406815),
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event1Metadata)!)),
metadata: [
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
"_HKPrivateWorkoutSegmentEventSubtype": NSNumber(1),
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1000),
"_HKPrivateMetadataSplitMeasuringSystem": NSNumber(1),
"_HKPrivateMetadataIsPartialSplit": NSNumber(0),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1114.56)
]),
.init(type: .init(rawValue: 7)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
duration: 1972.17168283463),
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
metadata: [
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17),
"_HKPrivateMetadataIsPartialSplit": 0,
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateWorkoutSegmentEventSubtype": 1,
"_HKPrivateMetadataSplitMeasuringSystem": 2
]),
.init(type: .init(rawValue: 1)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
duration: 0.0),
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
metadata: [
"_HKPrivateMetadataSplitDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateMetadataSplitActiveDurationQuantity": HKQuantity(unit: .second(), doubleValue: 1972.17),
"_HKPrivateMetadataIsPartialSplit": 0,
"_HKPrivateMetadataTotalDistanceQuantity": HKQuantity(unit: .meter(), doubleValue: 1609.34),
"_HKPrivateWorkoutSegmentEventSubtype": 1,
"_HKPrivateMetadataSplitMeasuringSystem": 2
]),
.init(type: .init(rawValue: 2)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),
duration: 0.0),
@ -24,7 +45,3 @@ extension HKWorkoutEvent {
]
}
}
private let mock1Event1Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c090000000000408f4012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c098d1f2246416a91401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c090000000000408f4012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2001"
private let mock1Event2Metadata = "0a370a275f484b507269766174654d65746164617461546f74616c44697374616e63655175616e74697479320c094c3789416025994012016d0a240a205f484b507269766174654d6574616461746149735061727469616c53706c697420000a3d0a2d5f484b507269766174654d6574616461746153706c69744163746976654475726174696f6e5175616e74697479320c09882da1cdafd09e401201730a370a275f484b507269766174654d6574616461746153706c697444697374616e63655175616e74697479320c094c3789416025994012016d0a280a245f484b50726976617465576f726b6f75745365676d656e744576656e745375627479706520010a2a0a265f484b507269766174654d6574616461746153706c69744d6561737572696e6753797374656d2002"

View File

@ -1,10 +1,11 @@
import SwiftUI
import HealthKit
struct SampleListView: View {
let type: Sample.DataType
let type: HKSampleType
let samples: [Sample]
let samples: [HKSample]
var body: some View {
List {
@ -21,9 +22,9 @@ struct SampleListView: View {
}
*/
extension Sample: Identifiable {
extension HKSample: Identifiable {
var id: Date {
startDate
public var id: UUID {
uuid
}
}

View File

@ -1,187 +0,0 @@
import Foundation
import HealthKit
extension HKWorkoutActivityType: CustomStringConvertible {
public var description: String {
switch self {
case .climbing:
return "Climbing"
case .cycling:
return "Cycling"
case .hiking:
return "Hiking"
case .hockey:
return "Hockey"
case .other:
return "Other"
case .rowing:
return "Rowing"
case .running:
return "Running"
case .swimming:
return "Swimming"
case .yoga:
return "Yoga"
case .walking:
return "Walking"
case .americanFootball:
return "American Football"
case .archery:
return "Archery"
case .australianFootball:
return "Australian Football"
case .badminton:
return "Badminton"
case .baseball:
return "Baseball"
case .basketball:
return "Basketball"
case .bowling:
return "Bowling"
case .boxing:
return "Boxing"
case .cricket:
return "Cricket"
case .crossTraining:
return "Cross Training"
case .curling:
return "Curling"
case .dance:
return "Dance"
case .danceInspiredTraining:
return "Dance Inspired Training"
case .elliptical:
return "Elliptical"
case .equestrianSports:
return "Equestrian Sports"
case .fencing:
return "Fencing"
case .fishing:
return "Fishing"
case .functionalStrengthTraining:
return "Functional Strength Training"
case .golf:
return "Golf"
case .gymnastics:
return "Gymnastics"
case .handball:
return "Handball"
case .hunting:
return "Hunting"
case .lacrosse:
return "Lacrosse"
case .martialArts:
return "Martial Arts"
case .mindAndBody:
return "Mind And Body"
case .mixedMetabolicCardioTraining:
return "Mixed Metabolic Cardio Training"
case .paddleSports:
return "Paddle Sports"
case .play:
return "Play"
case .preparationAndRecovery:
return "Preparation and Recovery"
case .racquetball:
return "Racquetball"
case .rugby:
return "Rugby"
case .sailing:
return "Sailing"
case .skatingSports:
return "Skating Sports"
case .snowSports:
return "Snow Sports"
case .soccer:
return "Soccer"
case .softball:
return "Softball"
case .squash:
return "Squash"
case .stairClimbing:
return "Stair Climbing"
case .surfingSports:
return "Surfing Sports"
case .tableTennis:
return "Table Tennis"
case .tennis:
return "Tennis"
case .trackAndField:
return "Track And Field"
case .traditionalStrengthTraining:
return "Traditional Strength Training"
case .volleyball:
return "Volleyball"
case .waterFitness:
return "Water Fitness"
case .waterPolo:
return "Water Polo"
case .waterSports:
return "Water Sports"
case .wrestling:
return "Wrestling"
case .barre:
return "Barre"
case .coreTraining:
return "Core Training"
case .crossCountrySkiing:
return "Cross Country Skiing"
case .downhillSkiing:
return "Downholl Skiing"
case .flexibility:
return "Flexibility"
case .highIntensityIntervalTraining:
return "High Intensity Interval Training"
case .jumpRope:
return "Jump Rope"
case .kickboxing:
return "Kickboxing"
case .pilates:
return "Pilates"
case .snowboarding:
return "Snowboarding"
case .stairs:
return "Stairs"
case .stepTraining:
return "Step Training"
case .wheelchairWalkPace:
return "Wheelchair Walk Pace"
case .wheelchairRunPace:
return "Wheelchair Run Pace"
case .taiChi:
return "Tai Chi"
case .mixedCardio:
return "Mixed Cardio"
case .handCycling:
return "Hand Cycling"
case .discSports:
return "Disc Sports"
case .fitnessGaming:
return "Fitness Gaming"
case .cardioDance:
return "Cardio Dance"
case .socialDance:
return "Social Dance"
case .pickleball:
return "Pickleball"
case .cooldown:
return "Cooldown"
case .swimBikeRun:
return "Triathlon"
case .transition:
return "Transition"
case .underwaterDiving:
return "Underwater Diving"
@unknown default:
return "\(rawValue)"
}
}
}
extension HKWorkoutActivityType: Comparable {
public static func < (lhs: HKWorkoutActivityType, rhs: HKWorkoutActivityType) -> Bool {
lhs.rawValue < rhs.rawValue
}
}

View File

@ -1,20 +0,0 @@
import Foundation
import HealthKit
extension HKWorkoutEventType: CustomStringConvertible {
public var description: String {
switch self {
case .pause: return "Pause"
case .resume: return "Resume"
case .lap: return "Lap"
case .marker: return "Marker"
case .motionPaused: return "Motion Paused"
case .motionResumed: return "Motion Resumed"
case .segment: return "Segment"
case .pauseOrResumeRequest: return "Pause or Resume Request"
@unknown default:
return "Unknown"
}
}
}

View File

@ -1,18 +0,0 @@
import Foundation
import HealthKit
extension HKWorkoutSessionLocationType: CustomStringConvertible {
public var description: String {
switch self {
case .unknown:
return "Unknown"
case .indoor:
return "Indoor"
case .outdoor:
return "Outdoor"
@unknown default:
return "Unknown default"
}
}
}

View File

@ -1,18 +0,0 @@
import Foundation
import HealthKit
extension HKWorkoutSwimmingLocationType: CustomStringConvertible {
public var description: String {
switch self {
case .unknown:
return "Unknown"
case .pool:
return "Pool"
case .openWater:
return "Open Water"
@unknown default:
return "Unknown default"
}
}
}

View File

@ -0,0 +1,36 @@
import Foundation
import HKDatabase
private let df: DateFormatter = {
let df = DateFormatter()
df.timeZone = .current
df.dateStyle = .short
df.timeStyle = .short
return df
}()
extension Workout {
var typeString: String {
activities.first?.workoutConfiguration.activityType.description ?? "Unknown activity"
}
var dateString: String {
guard let firstAvailableDate else {
return "No date"
}
return df.string(from: firstAvailableDate)
}
var firstActivityDate: Date? {
activities.map { $0.startDate }.min()
}
var firstEventDate: Date? {
events.map { $0.dateInterval.start }.min()
}
var firstAvailableDate: Date? {
[firstEventDate, firstActivityDate].compactMap { $0 }.min()
}
}

View File

@ -1,22 +1,22 @@
import SwiftUI
import Collections
import HealthKit
import HKDatabase
struct WorkoutDetailView: View {
@EnvironmentObject
var database: HealthDatabase
let workout: Workout
var metadata: [(key: String, value: Any)] {
workout.metadata.sorted { $0.key }
}
var body: some View {
List {
Section("Info") {
DetailRow("ID", value: workout.id)
DetailRow("Total Distance", kilometer: workout.totalDistance)
DetailRow("Goal", value: workout.goal)
DetailRow("Condenser Version", value: workout.condenserVersion)
DetailRow("Condenser Date", date: workout.condenserDate)
}
if !workout.activities.isEmpty {
Section("Activities") {
@ -40,8 +40,8 @@ struct WorkoutDetailView: View {
}
if !workout.metadata.isEmpty {
Section("Metadata") {
ForEach(workout.metadata.elements, id:\.key) { (key, value) in
DetailRow(key.description, value: value)
ForEach(metadata, id:\.key) { (key, value) in
DetailRow("\(key)", value: "\(value)")
}
}
}
@ -49,7 +49,6 @@ struct WorkoutDetailView: View {
.navigationTitle(workout.typeString)
.navigationDestination(for: HKWorkoutActivity.self) { activity in
ActivityDetailView(activity: activity)
.environmentObject(database)
}
.navigationDestination(for: HKWorkoutEvent.self) { event in
EventDetailView(event: event)
@ -58,9 +57,9 @@ struct WorkoutDetailView: View {
}
#Preview {
NavigationStack {
HealthDatabase.shared = .mock()
return NavigationStack {
WorkoutDetailView(workout: .mock1)
.environmentObject(HealthDatabase.mock())
}
}