Restructure workout events table
This commit is contained in:
parent
da0e758b35
commit
03b4f84807
@ -15,9 +15,9 @@
|
|||||||
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002702B5C299900E7D4DB /* HealthDatabase.swift */; };
|
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002702B5C299900E7D4DB /* HealthDatabase.swift */; };
|
||||||
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; };
|
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; };
|
||||||
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; };
|
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; };
|
||||||
8850027B2B5C35BF00E7D4DB /* Workout+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */; };
|
8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */; };
|
||||||
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027E2B5C36A700E7D4DB /* Workout.swift */; };
|
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027E2B5C36A700E7D4DB /* Workout.swift */; };
|
||||||
885002852B5C7AD600E7D4DB /* WorkoutEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002842B5C7AD600E7D4DB /* WorkoutEvent.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 */; };
|
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */; };
|
||||||
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */; };
|
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */; };
|
||||||
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */; };
|
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */; };
|
||||||
@ -50,7 +50,7 @@
|
|||||||
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */; };
|
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */; };
|
||||||
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */; };
|
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */; };
|
||||||
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; };
|
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */; };
|
||||||
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */; };
|
E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */; };
|
||||||
E27BC6902B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */; };
|
E27BC6902B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */; };
|
||||||
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */; };
|
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */; };
|
||||||
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6932B5FD587003A8873 /* Workout+Mock.swift */; };
|
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6932B5FD587003A8873 /* Workout+Mock.swift */; };
|
||||||
@ -76,9 +76,9 @@
|
|||||||
8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthdb_secure.sqlite; 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>"; };
|
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>"; };
|
885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+SQLite.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>"; };
|
8850027E2B5C36A700E7D4DB /* Workout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workout.swift; sourceTree = "<group>"; };
|
||||||
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEvent.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>"; };
|
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>"; };
|
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = "<group>"; };
|
||||||
8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
@ -108,7 +108,7 @@
|
|||||||
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantitySamplesTable.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>"; };
|
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>"; };
|
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitySamplesView.swift; sourceTree = "<group>"; };
|
||||||
E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+SQLite.swift"; sourceTree = "<group>"; };
|
E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEventsTable.swift; sourceTree = "<group>"; };
|
||||||
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+SQLite.swift"; sourceTree = "<group>"; };
|
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+SQLite.swift"; sourceTree = "<group>"; };
|
||||||
E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HealthDatabase+Mock.swift"; sourceTree = "<group>"; };
|
E27BC6912B5FD488003A8873 /* HealthDatabase+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HealthDatabase+Mock.swift"; sourceTree = "<group>"; };
|
||||||
E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = "<group>"; };
|
E27BC6932B5FD587003A8873 /* Workout+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Mock.swift"; sourceTree = "<group>"; };
|
||||||
@ -216,10 +216,8 @@
|
|||||||
8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */,
|
8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */,
|
||||||
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
|
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
|
||||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||||
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
|
||||||
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */,
|
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */,
|
||||||
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */,
|
885002842B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift */,
|
||||||
E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */,
|
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -259,6 +257,8 @@
|
|||||||
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */,
|
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */,
|
||||||
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */,
|
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */,
|
||||||
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */,
|
E27BC6892B5FC255003A8873 /* UnitStringsTable.swift */,
|
||||||
|
E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */,
|
||||||
|
8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */,
|
||||||
);
|
);
|
||||||
path = Tables;
|
path = Tables;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -361,7 +361,7 @@
|
|||||||
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */,
|
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */,
|
||||||
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */,
|
E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */,
|
||||||
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
|
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
|
||||||
885002852B5C7AD600E7D4DB /* WorkoutEvent.swift in Sources */,
|
885002852B5C7AD600E7D4DB /* HKWorkoutEvent+Identifiable.swift in Sources */,
|
||||||
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
|
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
|
||||||
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */,
|
E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */,
|
||||||
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */,
|
E27BC6922B5FD488003A8873 /* HealthDatabase+Mock.swift in Sources */,
|
||||||
@ -375,7 +375,7 @@
|
|||||||
8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */,
|
8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */,
|
||||||
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */,
|
E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */,
|
||||||
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */,
|
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */,
|
||||||
8850027B2B5C35BF00E7D4DB /* Workout+SQLite.swift in Sources */,
|
8850027B2B5C35BF00E7D4DB /* WorkoutsTable.swift in Sources */,
|
||||||
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
|
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
|
||||||
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
|
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
|
||||||
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
||||||
@ -395,7 +395,7 @@
|
|||||||
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */,
|
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */,
|
||||||
E201EC812B631708005B83D3 /* Goal.swift in Sources */,
|
E201EC812B631708005B83D3 /* Goal.swift in Sources */,
|
||||||
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
|
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
|
||||||
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */,
|
E27BC68E2B5FCBD5003A8873 /* WorkoutEventsTable.swift in Sources */,
|
||||||
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -13,6 +13,8 @@ final class HealthDatabase: ObservableObject {
|
|||||||
|
|
||||||
private let samples: SamplesTable
|
private let samples: SamplesTable
|
||||||
|
|
||||||
|
private let workoutsTable: WorkoutsTable
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var workouts: [Workout] = []
|
var workouts: [Workout] = []
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ final class HealthDatabase: ObservableObject {
|
|||||||
self.fileUrl = fileUrl
|
self.fileUrl = fileUrl
|
||||||
self.database = database
|
self.database = database
|
||||||
self.samples = .init(database: database)
|
self.samples = .init(database: database)
|
||||||
|
self.workoutsTable = .init(database: database)
|
||||||
|
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
self.readAllWorkouts()
|
self.readAllWorkouts()
|
||||||
}
|
}
|
||||||
@ -33,7 +37,7 @@ final class HealthDatabase: ObservableObject {
|
|||||||
func readAllWorkouts() {
|
func readAllWorkouts() {
|
||||||
let workouts: [Workout]
|
let workouts: [Workout]
|
||||||
do {
|
do {
|
||||||
workouts = try WorkoutTable.readAll(in: database)
|
workouts = try workoutsTable.workouts()
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to read workouts: \(error)")
|
print("Failed to read workouts: \(error)")
|
||||||
return
|
return
|
||||||
@ -83,6 +87,10 @@ final class HealthDatabase: ObservableObject {
|
|||||||
self.init(fileUrl: .init(filePath: "/"), 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? {
|
func insert(workout: Workout, into store: HKHealthStore) async throws -> HKWorkout? {
|
||||||
guard let configuration = workout.activities.first?.workoutConfiguration else {
|
guard let configuration = workout.activities.first?.workoutConfiguration else {
|
||||||
return nil
|
return nil
|
||||||
@ -91,6 +99,11 @@ final class HealthDatabase: ObservableObject {
|
|||||||
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil)
|
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil)
|
||||||
return try await builder.finishWorkout()
|
return try await builder.finishWorkout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createTables() throws {
|
||||||
|
try samples.createAll()
|
||||||
|
try workoutsTable.createAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension HKWorkoutActivity {
|
private extension HKWorkoutActivity {
|
||||||
|
10
HealthImport/Model/HKWorkoutEvent+Identifiable.swift
Normal file
10
HealthImport/Model/HKWorkoutEvent+Identifiable.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
import HealthKit
|
||||||
|
import SwiftProtobuf
|
||||||
|
|
||||||
|
extension HKWorkoutEvent: Identifiable {
|
||||||
|
|
||||||
|
public var id: Double {
|
||||||
|
dateInterval.start.timeIntervalSinceReferenceDate * Double(type.rawValue) * dateInterval.duration
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ struct ObjectsTable {
|
|||||||
self.database = database
|
self.database = database
|
||||||
}
|
}
|
||||||
|
|
||||||
func create() throws {
|
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)")
|
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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ struct QuantitySamplesTable {
|
|||||||
self.database = database
|
self.database = database
|
||||||
}
|
}
|
||||||
|
|
||||||
func create() throws {
|
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)")
|
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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,14 @@ struct SamplesTable {
|
|||||||
try database.execute("CREATE TABLE samples (data_id INTEGER PRIMARY KEY, start_date REAL, end_date REAL, data_type INTEGER)")
|
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 table = Table("samples")
|
||||||
|
|
||||||
private let dataId = Expression<Int>("data_id")
|
private let dataId = Expression<Int>("data_id")
|
||||||
|
174
HealthImport/Model/Tables/WorkoutEventsTable.swift
Normal file
174
HealthImport/Model/Tables/WorkoutEventsTable.swift
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
HealthImport/Model/Tables/WorkoutsTable.swift
Normal file
97
HealthImport/Model/Tables/WorkoutsTable.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import Foundation
|
||||||
|
import SQLite
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
|
struct WorkoutsTable {
|
||||||
|
|
||||||
|
private let database: Connection
|
||||||
|
|
||||||
|
let events: WorkoutEventsTable
|
||||||
|
|
||||||
|
init(database: Connection) {
|
||||||
|
self.database = database
|
||||||
|
self.events = .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] {
|
||||||
|
let metadataKeys = try Metadata.allKeys(in: database)
|
||||||
|
|
||||||
|
return try database.prepare(table).map { row in
|
||||||
|
let id = row[dataId]
|
||||||
|
|
||||||
|
let events = try events.events(for: id, in: database)
|
||||||
|
let activities = try WorkoutActivityTable.activities(for: id, in: database)
|
||||||
|
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 WorkoutActivityTable.insert(activity, isPrimaryActivity: true, dataId: dataId, in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in element.metadata {
|
||||||
|
try Metadata.insert(value, for: key, of: dataId, in: database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SQLite
|
|
||||||
import HealthKit
|
|
||||||
|
|
||||||
enum WorkoutTable {
|
|
||||||
|
|
||||||
private static let table = Table("workouts")
|
|
||||||
|
|
||||||
// INTEGER PRIMARY KEY AUTOINCREMENT
|
|
||||||
private static let columnDataId = Expression<Int>("data_id")
|
|
||||||
|
|
||||||
// REAL
|
|
||||||
private static let columnTotalDistance = Expression<Double?>("total_distance")
|
|
||||||
|
|
||||||
// INTEGER
|
|
||||||
private static let columnGoalType = Expression<Int?>("goal_type")
|
|
||||||
|
|
||||||
// REAL
|
|
||||||
private static let columnGoal = Expression<Double?>("goal")
|
|
||||||
|
|
||||||
// INTEGER
|
|
||||||
private static let columnCondenserVersion = Expression<Int?>("condenser_version")
|
|
||||||
|
|
||||||
// REAL
|
|
||||||
private static let columnCondenserDate = Expression<Double?>("condenser_date")
|
|
||||||
|
|
||||||
static func readAll(in database: Connection) throws -> [Workout] {
|
|
||||||
let metadataKeys = try Metadata.allKeys(in: database)
|
|
||||||
|
|
||||||
return try database.prepare(table).map { row in
|
|
||||||
let id = row[columnDataId]
|
|
||||||
|
|
||||||
let events = try HKWorkoutEventTable.events(for: id, in: database)
|
|
||||||
let activities = try WorkoutActivityTable.activities(for: id, in: database)
|
|
||||||
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
|
|
||||||
return .init(
|
|
||||||
id: id,
|
|
||||||
totalDistance: row[columnTotalDistance],
|
|
||||||
goalType: row[columnGoalType],
|
|
||||||
goal: row[columnGoal],
|
|
||||||
condenserVersion: row[columnCondenserVersion],
|
|
||||||
condenserDate: row[columnCondenserDate].map { Date.init(timeIntervalSinceReferenceDate: $0) },
|
|
||||||
events: events,
|
|
||||||
activities: activities,
|
|
||||||
metadata: metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func create(in database: Database) throws {
|
|
||||||
try database.run(table.create { t in
|
|
||||||
t.column(columnDataId, primaryKey: .autoincrement)
|
|
||||||
t.column(columnTotalDistance)
|
|
||||||
t.column(columnGoalType)
|
|
||||||
t.column(columnGoal)
|
|
||||||
t.column(columnCondenserVersion)
|
|
||||||
t.column(columnCondenserDate)
|
|
||||||
})
|
|
||||||
// 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)")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func insert(_ element: Workout, in database: Database) throws {
|
|
||||||
let rowid = try database.run(table.insert(
|
|
||||||
columnTotalDistance <- element.totalDistance,
|
|
||||||
columnGoalType <- element.goal?.goalType,
|
|
||||||
columnGoal <- element.goal?.gaol,
|
|
||||||
columnCondenserVersion <- element.condenserVersion,
|
|
||||||
columnCondenserDate <- element.condenserDate?.timeIntervalSinceReferenceDate)
|
|
||||||
)
|
|
||||||
let dataId = Int(rowid)
|
|
||||||
for event in element.events {
|
|
||||||
try event.insert(in: database, dataId: dataId)
|
|
||||||
}
|
|
||||||
|
|
||||||
for activity in element.activities {
|
|
||||||
try WorkoutActivityTable.insert(activity, isPrimaryActivity: true, dataId: dataId, in: database)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in element.metadata {
|
|
||||||
try Metadata.insert(value, for: key, of: dataId, in: database)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SQLite
|
|
||||||
import HealthKit
|
|
||||||
|
|
||||||
enum HKWorkoutEventTable {
|
|
||||||
|
|
||||||
private static let table = Table("workout_events")
|
|
||||||
|
|
||||||
// INTEGER PRIMARY KEY AUTOINCREMENT
|
|
||||||
private static let columnRowId = Expression<Int>("ROW_ID")
|
|
||||||
|
|
||||||
// owner_id INTEGER NOT NULL REFERENCES workouts (data_id) ON DELETE CASCADE
|
|
||||||
private static let columnOwnerId = Expression<Int>("owner_id")
|
|
||||||
|
|
||||||
// date REAL NOT NULL
|
|
||||||
private static let columnDate = Expression<Double>("date")
|
|
||||||
|
|
||||||
// type INTEGER NOT NULL
|
|
||||||
private static let columnType = Expression<Int>("type")
|
|
||||||
|
|
||||||
// duration REAL NOT NULL
|
|
||||||
private static let columnDuration = Expression<Double>("duration")
|
|
||||||
|
|
||||||
// metadata BLOB
|
|
||||||
private static let columnMetadata = Expression<Data?>("metadata")
|
|
||||||
|
|
||||||
// session_uuid BLOB
|
|
||||||
private static let columnSessionUUID = Expression<Data?>("session_uuid")
|
|
||||||
|
|
||||||
// error BLOB
|
|
||||||
private static let columnError = Expression<Data?>("error")
|
|
||||||
|
|
||||||
static func readAll(in database: Connection) throws -> [HKWorkoutEvent] {
|
|
||||||
try database.prepare(table).map(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func events(for workoutId: Int, in database: Connection) throws -> [HKWorkoutEvent] {
|
|
||||||
try database.prepare(table.filter(columnOwnerId == workoutId)).map(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func event(from row: Row) -> HKWorkoutEvent {
|
|
||||||
let start = Date(timeIntervalSinceReferenceDate: row[columnDate])
|
|
||||||
let interval = DateInterval(start: start, duration: row[columnDuration])
|
|
||||||
let metadata = metadata(row[columnMetadata])
|
|
||||||
let type = HKWorkoutEventType(rawValue: row[columnType])!
|
|
||||||
// let sessionUUID = row[columnSessionUUID]
|
|
||||||
// let error = row[columnError]
|
|
||||||
return .init(type: type, dateInterval: interval, metadata: metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func metadata(_ data: Data?) -> [String : Any] {
|
|
||||||
guard let data else {
|
|
||||||
return [:]
|
|
||||||
}
|
|
||||||
return decode(metadata: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func create(in database: Database) 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(columnRowId, primaryKey: .autoincrement)
|
|
||||||
t.column(columnOwnerId, references: Table("workouts"), Expression<Int>("data_id"))
|
|
||||||
t.column(columnDate)
|
|
||||||
t.column(columnType)
|
|
||||||
t.column(columnDuration)
|
|
||||||
t.column(columnMetadata)
|
|
||||||
t.column(columnSessionUUID)
|
|
||||||
t.column(columnError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static func insert(_ element: HKWorkoutEvent, dataId: Int, in database: Database) throws {
|
|
||||||
try database.run(table.insert(
|
|
||||||
columnOwnerId <- dataId,
|
|
||||||
columnDate <- element.dateInterval.start.timeIntervalSinceReferenceDate,
|
|
||||||
columnType <- element.type.rawValue,
|
|
||||||
columnDuration <- element.dateInterval.duration,
|
|
||||||
columnMetadata <- encode(metadata: element.metadata ?? [:]))
|
|
||||||
// columnSessionUUID <- element.sessionUUID
|
|
||||||
// columnError <- element.error)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HKWorkoutEvent {
|
|
||||||
|
|
||||||
func insert(in database: Database, dataId: Int) throws {
|
|
||||||
try HKWorkoutEventTable.insert(self, dataId: dataId, in: database)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import HealthKit
|
|
||||||
import SwiftProtobuf
|
|
||||||
|
|
||||||
extension HKWorkoutEvent: Identifiable {
|
|
||||||
|
|
||||||
public var id: Double {
|
|
||||||
dateInterval.start.timeIntervalSinceReferenceDate * Double(type.rawValue) * dateInterval.duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HKWorkoutEventTable {
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,23 +7,14 @@ extension HealthDatabase {
|
|||||||
static func mock() -> HealthDatabase {
|
static func mock() -> HealthDatabase {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let database = try makeDatabase()
|
let connection = try Connection(.inMemory)
|
||||||
return .init(database: database)
|
let database = HealthDatabase(database: connection)
|
||||||
|
try database.createTables()
|
||||||
|
try database.insert(workout: .mock1)
|
||||||
|
return database
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
fatalError("Failed to create mock database: \(error)")
|
fatalError("Failed to create mock database: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeDatabase() throws -> Connection {
|
|
||||||
let database = try Connection(.inMemory)
|
|
||||||
|
|
||||||
try WorkoutTable.create(in: database)
|
|
||||||
try HKWorkoutEventTable.create(in: database)
|
|
||||||
try WorkoutActivityTable.create(in: database)
|
|
||||||
try Metadata.createTables(in: database)
|
|
||||||
|
|
||||||
try WorkoutTable.insert(.mock1, in: database)
|
|
||||||
return database
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,15 @@ extension HKWorkoutEvent {
|
|||||||
.init(type: .init(rawValue: 7)!,
|
.init(type: .init(rawValue: 7)!,
|
||||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||||
duration: 1114.56374406815),
|
duration: 1114.56374406815),
|
||||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event1Metadata)!)),
|
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event1Metadata)!)),
|
||||||
.init(type: .init(rawValue: 7)!,
|
.init(type: .init(rawValue: 7)!,
|
||||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||||
duration: 1972.17168283463),
|
duration: 1972.17168283463),
|
||||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||||
.init(type: .init(rawValue: 1)!,
|
.init(type: .init(rawValue: 1)!,
|
||||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
|
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
|
||||||
duration: 0.0),
|
duration: 0.0),
|
||||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||||
.init(type: .init(rawValue: 2)!,
|
.init(type: .init(rawValue: 2)!,
|
||||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),
|
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),
|
||||||
duration: 0.0),
|
duration: 0.0),
|
||||||
|
Loading…
Reference in New Issue
Block a user