Restructure workout events table
This commit is contained in:
@@ -13,6 +13,8 @@ final class HealthDatabase: ObservableObject {
|
||||
|
||||
private let samples: SamplesTable
|
||||
|
||||
private let workoutsTable: WorkoutsTable
|
||||
|
||||
@Published
|
||||
var workouts: [Workout] = []
|
||||
|
||||
@@ -25,6 +27,8 @@ final class HealthDatabase: ObservableObject {
|
||||
self.fileUrl = fileUrl
|
||||
self.database = database
|
||||
self.samples = .init(database: database)
|
||||
self.workoutsTable = .init(database: database)
|
||||
|
||||
DispatchQueue.global().async {
|
||||
self.readAllWorkouts()
|
||||
}
|
||||
@@ -33,7 +37,7 @@ final class HealthDatabase: ObservableObject {
|
||||
func readAllWorkouts() {
|
||||
let workouts: [Workout]
|
||||
do {
|
||||
workouts = try WorkoutTable.readAll(in: database)
|
||||
workouts = try workoutsTable.workouts()
|
||||
} catch {
|
||||
print("Failed to read workouts: \(error)")
|
||||
return
|
||||
@@ -83,6 +87,10 @@ final class HealthDatabase: ObservableObject {
|
||||
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
|
||||
@@ -91,6 +99,11 @@ final class HealthDatabase: ObservableObject {
|
||||
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: nil)
|
||||
return try await builder.finishWorkout()
|
||||
}
|
||||
|
||||
func createTables() throws {
|
||||
try samples.createAll()
|
||||
try workoutsTable.createAll()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ struct QuantitySamplesTable {
|
||||
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)")
|
||||
}
|
||||
|
||||
|
@@ -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)")
|
||||
}
|
||||
|
||||
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")
|
||||
|
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 {
|
||||
|
||||
do {
|
||||
let database = try makeDatabase()
|
||||
return .init(database: database)
|
||||
let connection = try Connection(.inMemory)
|
||||
let database = HealthDatabase(database: connection)
|
||||
try database.createTables()
|
||||
try database.insert(workout: .mock1)
|
||||
return database
|
||||
} catch {
|
||||
print(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)!,
|
||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||
duration: 1114.56374406815),
|
||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event1Metadata)!)),
|
||||
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event1Metadata)!)),
|
||||
.init(type: .init(rawValue: 7)!,
|
||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||
duration: 1972.17168283463),
|
||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||
.init(type: .init(rawValue: 1)!,
|
||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
|
||||
duration: 0.0),
|
||||
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||
metadata: WorkoutEventsTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
|
||||
.init(type: .init(rawValue: 2)!,
|
||||
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),
|
||||
duration: 0.0),
|
||||
|
Reference in New Issue
Block a user