Switch to HKWorkoutActivity
This commit is contained in:
parent
b8162e6cb9
commit
94fc10f204
@ -19,7 +19,6 @@
|
|||||||
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 /* WorkoutEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002842B5C7AD600E7D4DB /* WorkoutEvent.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 */; };
|
||||||
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028A2B5C896C00E7D4DB /* WorkoutActivity.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 */; };
|
||||||
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */; };
|
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */; };
|
||||||
@ -78,7 +77,6 @@
|
|||||||
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 /* WorkoutEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEvent.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>"; };
|
||||||
8850028A2B5C896C00E7D4DB /* WorkoutActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivity.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>"; };
|
||||||
885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEventType+Extensions.swift"; sourceTree = "<group>"; };
|
885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutEventType+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
@ -215,7 +213,6 @@
|
|||||||
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */,
|
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */,
|
||||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||||
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
||||||
8850028A2B5C896C00E7D4DB /* WorkoutActivity.swift */,
|
|
||||||
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */,
|
E27BC68F2B5FCEA4003A8873 /* WorkoutActivity+SQLite.swift */,
|
||||||
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */,
|
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */,
|
||||||
E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */,
|
E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift */,
|
||||||
@ -367,7 +364,6 @@
|
|||||||
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */,
|
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */,
|
||||||
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
|
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
|
||||||
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */,
|
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */,
|
||||||
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */,
|
|
||||||
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */,
|
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */,
|
||||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
||||||
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
|
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
struct ActivityDetailView: View {
|
struct ActivityDetailView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var database: HealthDatabase
|
var database: HealthDatabase
|
||||||
|
|
||||||
let activity: WorkoutActivity
|
let activity: HKWorkoutActivity
|
||||||
|
|
||||||
@State var locations: [LocationSample] = []
|
@State var locations: [LocationSample] = []
|
||||||
|
|
||||||
@ -14,11 +15,10 @@ struct ActivityDetailView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
DetailRow("UUID", value: activity.uuid)
|
DetailRow("UUID", value: activity.uuid)
|
||||||
DetailRow("Primary Activity", value: activity.isPrimaryActivity)
|
DetailRow("Activity", value: activity.workoutConfiguration.activityType)
|
||||||
DetailRow("Activity", value: activity.activityType)
|
DetailRow("Location", value: activity.workoutConfiguration.locationType)
|
||||||
DetailRow("Location", value: activity.locationType)
|
DetailRow("Swimming Location", value: activity.workoutConfiguration.swimmingLocationType)
|
||||||
DetailRow("Swimming Location", value: activity.swimmingLocationType)
|
DetailRow("Lap Length", value: activity.workoutConfiguration.lapLength)
|
||||||
DetailRow("Lap Length", value: activity.lapLength)
|
|
||||||
DetailRow("Start", date: activity.startDate)
|
DetailRow("Start", date: activity.startDate)
|
||||||
DetailRow("End", date: activity.endDate)
|
DetailRow("End", date: activity.endDate)
|
||||||
DetailRow("Duration", duration: activity.duration)
|
DetailRow("Duration", duration: activity.duration)
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import OrderedCollections
|
import OrderedCollections
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
struct ActivitySamplesView: View {
|
struct ActivitySamplesView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var database: HealthDatabase
|
var database: HealthDatabase
|
||||||
|
|
||||||
let activity: WorkoutActivity
|
let activity: HKWorkoutActivity
|
||||||
|
|
||||||
@State var samples: [(type: Sample.DataType, samples: [Sample])] = []
|
@State var samples: [(type: Sample.DataType, samples: [Sample])] = []
|
||||||
|
|
||||||
init(activity: WorkoutActivity) {
|
init(activity: HKWorkoutActivity) {
|
||||||
self.activity = activity
|
self.activity = activity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SQLite
|
import SQLite
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
typealias Database = Connection
|
typealias Database = Connection
|
||||||
|
|
||||||
@ -39,19 +40,25 @@ final class HealthDatabase: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationSamples(for activity: WorkoutActivity) throws -> [LocationSample] {
|
func locationSamples(for activity: HKWorkoutActivity) throws -> [LocationSample] {
|
||||||
try activity.locationSamples(in: database)
|
try LocationSample.locationSamples(from: activity.startDate, to: activity.currentEndDate, in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func samples(for activity: WorkoutActivity) throws -> [Sample.DataType : [Sample]] {
|
func locationSampleCount(for activity: HKWorkoutActivity) throws -> Int {
|
||||||
try activity.samples(in: database)
|
try LocationSample.locationSampleCount(from: activity.startDate, to: activity.currentEndDate, in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sampleCount(for activity: WorkoutActivity) throws -> Int {
|
func samples(for activity: HKWorkoutActivity) throws -> [Sample.DataType : [Sample]] {
|
||||||
try activity.sampleCount(in: database)
|
try Sample.samples(from: activity.startDate, to: activity.currentEndDate, in: database).reduce(into: [:]) {
|
||||||
|
$0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var activities: [WorkoutActivity] {
|
func sampleCount(for activity: HKWorkoutActivity) throws -> Int {
|
||||||
|
try Sample.sampleCount(from: activity.startDate, to: activity.currentEndDate, in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
var activities: [HKWorkoutActivity] {
|
||||||
workouts.map { $0.activities }.joined().sorted()
|
workouts.map { $0.activities }.joined().sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,11 +66,11 @@ final class HealthDatabase: ObservableObject {
|
|||||||
let activities = self.activities
|
let activities = self.activities
|
||||||
var current = activities.first!
|
var current = activities.first!
|
||||||
for next in activities.dropFirst() {
|
for next in activities.dropFirst() {
|
||||||
let overlap = next.startDate.timeIntervalSince(current.endDate)
|
let overlap = next.startDate.timeIntervalSince(current.currentEndDate)
|
||||||
if overlap < 0 {
|
if overlap < 0 {
|
||||||
print("Overlap \(-overlap.roundedInt) s:")
|
print("Overlap \(-overlap.roundedInt) s:")
|
||||||
print(" Activity \(current.activityType.description): \(current.startDate.timeAndDateText) -> \(current.endDate.timeAndDateText)")
|
print(" Activity \(current.workoutConfiguration.activityType.description): \(current.startDate.timeAndDateText) -> \(current.currentEndDate.timeAndDateText)")
|
||||||
print(" Activity \(next.activityType.description): \(next.startDate.timeAndDateText) -> \(next.endDate.timeAndDateText)")
|
print(" Activity \(next.workoutConfiguration.activityType.description): \(next.startDate.timeAndDateText) -> \(next.currentEndDate.timeAndDateText)")
|
||||||
}
|
}
|
||||||
current = next
|
current = next
|
||||||
}
|
}
|
||||||
@ -73,3 +80,10 @@ final class HealthDatabase: ObservableObject {
|
|||||||
self.init(fileUrl: .init(filePath: "/"), database: database)
|
self.init(fileUrl: .init(filePath: "/"), database: database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension HKWorkoutActivity {
|
||||||
|
|
||||||
|
var currentEndDate: Date {
|
||||||
|
endDate ?? Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ extension Workout {
|
|||||||
let id = row[columnDataId]
|
let id = row[columnDataId]
|
||||||
|
|
||||||
let events = try HKWorkoutEventTable.events(for: id, in: database)
|
let events = try HKWorkoutEventTable.events(for: id, in: database)
|
||||||
let activities = try WorkoutActivity.activities(for: id, in: database)
|
let activities = try WorkoutActivityTable.activities(for: id, in: database)
|
||||||
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
|
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
|
||||||
return .init(
|
return .init(
|
||||||
id: id,
|
id: id,
|
||||||
@ -74,9 +74,14 @@ extension Workout {
|
|||||||
for event in element.events {
|
for event in element.events {
|
||||||
try event.insert(in: database, dataId: dataId)
|
try event.insert(in: database, dataId: dataId)
|
||||||
}
|
}
|
||||||
for activity in element.activities {
|
|
||||||
try activity.insert(in: database, dataId: dataId)
|
if let activity = element.activities.first {
|
||||||
|
try WorkoutActivityTable.insert(activity, isPrimaryActivity: true, dataId: dataId, in: database)
|
||||||
}
|
}
|
||||||
|
for activity in element.activities.dropFirst() {
|
||||||
|
try WorkoutActivityTable.insert(activity, isPrimaryActivity: false, dataId: dataId, in: database)
|
||||||
|
}
|
||||||
|
|
||||||
for (key, value) in element.metadata {
|
for (key, value) in element.metadata {
|
||||||
try Metadata.insert(value, for: key, of: dataId, in: database)
|
try Metadata.insert(value, for: key, of: dataId, in: database)
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ struct Workout {
|
|||||||
|
|
||||||
let events: [HKWorkoutEvent]
|
let events: [HKWorkoutEvent]
|
||||||
|
|
||||||
let activities: [WorkoutActivity]
|
let activities: [HKWorkoutActivity]
|
||||||
|
|
||||||
let metadata: OrderedDictionary<Metadata.Key, Metadata.Value>
|
let metadata: OrderedDictionary<Metadata.Key, Metadata.Value>
|
||||||
|
|
||||||
@ -49,10 +49,10 @@ struct Workout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var typeString: String {
|
var typeString: String {
|
||||||
activities.first?.activityType.description ?? "Unknown activity"
|
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: [WorkoutActivity] = [], metadata: [Metadata.Key : Metadata.Value] = [:]) {
|
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.id = id
|
||||||
self.totalDistance = totalDistance
|
self.totalDistance = totalDistance
|
||||||
self.goal = .init(goalType: goalType, goal: goal)
|
self.goal = .init(goalType: goalType, goal: goal)
|
||||||
|
@ -2,7 +2,14 @@ import Foundation
|
|||||||
import SQLite
|
import SQLite
|
||||||
import HealthKit
|
import HealthKit
|
||||||
|
|
||||||
extension WorkoutActivity {
|
extension HKWorkoutActivity: Comparable {
|
||||||
|
|
||||||
|
public static func < (lhs: HKWorkoutActivity, rhs: HKWorkoutActivity) -> Bool {
|
||||||
|
lhs.startDate < rhs.startDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WorkoutActivityTable {
|
||||||
|
|
||||||
private static let table = Table("workout_activities")
|
private static let table = Table("workout_activities")
|
||||||
|
|
||||||
@ -30,30 +37,38 @@ extension WorkoutActivity {
|
|||||||
|
|
||||||
private static let columnMetadata = Expression<Data?>("metadata")
|
private static let columnMetadata = Expression<Data?>("metadata")
|
||||||
|
|
||||||
private static func readAll(in database: Connection) throws -> [Self] {
|
private static func readAll(in database: Connection) throws -> [HKWorkoutActivity] {
|
||||||
try database.prepare(table).map(from)
|
try database.prepare(table).map(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func from(row: Row) throws -> WorkoutActivity {
|
private static func activity(from row: Row) throws -> HKWorkoutActivity {
|
||||||
.init(
|
let configuration = HKWorkoutConfiguration()
|
||||||
id: row[columnId],
|
configuration.lapLength = try row[columnLapLength].map(lapLength)
|
||||||
uuid: row[columnUUID],
|
configuration.activityType = .init(rawValue: UInt(row[columnActivityType]))!
|
||||||
isPrimaryActivity: row[columnIsPrimaryActivity],
|
configuration.locationType = .init(rawValue: row[columnLocationType])!
|
||||||
activityType: .init(rawValue: UInt(row[columnActivityType]))!,
|
configuration.swimmingLocationType = .init(rawValue: row[columnSwimmingLocationType])!
|
||||||
locationType: .init(rawValue: row[columnLocationType])!,
|
|
||||||
swimmingLocationType: .init(rawValue: row[columnSwimmingLocationType])!,
|
let start = Date(timeIntervalSinceReferenceDate: row[columnStartDate])
|
||||||
lapLength: try row[columnLapLength].map(lapLength),
|
let end = Date(timeIntervalSinceReferenceDate: row[columnEndDate])
|
||||||
startDate: Date(timeIntervalSinceReferenceDate: row[columnStartDate]),
|
let uuid = row[columnUUID].uuidString
|
||||||
endDate: Date(timeIntervalSinceReferenceDate: row[columnEndDate]),
|
let metadata: [String : Any] = [HKMetadataKeyExternalUUID : uuid]
|
||||||
duration: row[columnDuration],
|
// duration: row[columnDuration]
|
||||||
metadata: row[columnMetadata])
|
// isPrimaryActivity: row[columnIsPrimaryActivity]
|
||||||
|
|
||||||
|
// metadata: row[columnMetadata]
|
||||||
|
#warning("Decode metadata")
|
||||||
|
return .init(
|
||||||
|
workoutConfiguration: configuration,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
metadata: metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func activities(for workoutId: Int, in database: Connection) throws -> [Self] {
|
static func activities(for workoutId: Int, in database: Connection) throws -> [HKWorkoutActivity] {
|
||||||
try database.prepare(table.filter(columnOwnerId == workoutId)).map(from)
|
try database.prepare(table.filter(columnOwnerId == workoutId)).map(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createTable(in database: Connection) throws {
|
static func create(in database: Connection) 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.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
|
try database.run(table.create { t in
|
||||||
t.column(columnId, primaryKey: .autoincrement)
|
t.column(columnId, primaryKey: .autoincrement)
|
||||||
@ -71,30 +86,36 @@ extension WorkoutActivity {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(in database: Connection, dataId: Int) throws {
|
static func insert(_ element: HKWorkoutActivity, isPrimaryActivity: Bool, dataId: Int, in database: Connection) throws {
|
||||||
try WorkoutActivity.insert(self, dataId: dataId, in: database)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func insert(_ element: WorkoutActivity, dataId: Int, in database: Connection) throws {
|
|
||||||
try database.run(table.insert(
|
try database.run(table.insert(
|
||||||
columnUUID <- element.uuid,
|
columnUUID <- (element.externalUUID ?? element.uuid).uuidString.data(using: .utf8)!,
|
||||||
columnOwnerId <- dataId,
|
columnOwnerId <- dataId,
|
||||||
columnIsPrimaryActivity <- element.isPrimaryActivity,
|
columnIsPrimaryActivity <- isPrimaryActivity,
|
||||||
columnActivityType <- Int(element.activityType.rawValue),
|
columnActivityType <- Int(element.workoutConfiguration.activityType.rawValue),
|
||||||
columnLocationType <- element.locationType.rawValue,
|
columnLocationType <- element.workoutConfiguration.locationType.rawValue,
|
||||||
columnSwimmingLocationType <- element.swimmingLocationType.rawValue,
|
columnSwimmingLocationType <- element.workoutConfiguration.swimmingLocationType.rawValue,
|
||||||
columnLapLength <- try element.lapLengthData(),
|
columnLapLength <- try lapLengthData(lapLength: element.workoutConfiguration.lapLength),
|
||||||
columnStartDate <- element.startDate.timeIntervalSinceReferenceDate,
|
columnStartDate <- element.startDate.timeIntervalSinceReferenceDate,
|
||||||
columnEndDate <- element.endDate.timeIntervalSinceReferenceDate,
|
columnEndDate <- element.endDate?.timeIntervalSinceReferenceDate ?? element.startDate.addingTimeInterval(element.duration).timeIntervalSinceReferenceDate,
|
||||||
columnDuration <- element.duration,
|
columnDuration <- element.duration)
|
||||||
columnMetadata <- element.metadata)
|
//columnMetadata <- element.metadata)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension WorkoutActivity {
|
private extension HKWorkoutActivity {
|
||||||
|
|
||||||
func lapLengthData() throws -> Data? {
|
var externalUUID: UUID? {
|
||||||
|
guard let string = metadata?[HKMetadataKeyExternalUUID] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UUID(uuidString: string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension WorkoutActivityTable {
|
||||||
|
|
||||||
|
static func lapLengthData(lapLength: HKQuantity?) throws -> Data? {
|
||||||
try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
|
try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,3 +123,19 @@ private extension WorkoutActivity {
|
|||||||
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
|
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension Data {
|
||||||
|
|
||||||
|
var uuidString: String {
|
||||||
|
let h = Array(self)
|
||||||
|
let parts = [h[0..<4], h[4..<6], h[6..<8], h[8..<10], h[10..<16]]
|
||||||
|
return parts.map { Data($0).hex }.joined(separator: "-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UUID {
|
||||||
|
|
||||||
|
init?(data: Data) {
|
||||||
|
self.init(uuidString: data.uuidString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import HealthKit
|
|
||||||
|
|
||||||
struct WorkoutActivity {
|
|
||||||
|
|
||||||
private let id: Int
|
|
||||||
|
|
||||||
let uuid: Data
|
|
||||||
|
|
||||||
let isPrimaryActivity: Bool
|
|
||||||
|
|
||||||
let activityType: HKWorkoutActivityType
|
|
||||||
|
|
||||||
let locationType: HKWorkoutSessionLocationType
|
|
||||||
|
|
||||||
let swimmingLocationType: HKWorkoutSwimmingLocationType
|
|
||||||
|
|
||||||
let lapLength: HKQuantity?
|
|
||||||
|
|
||||||
#warning("Fix timezone for dates")
|
|
||||||
let startDate: Date
|
|
||||||
|
|
||||||
let endDate: Date
|
|
||||||
|
|
||||||
let duration: TimeInterval
|
|
||||||
|
|
||||||
let metadata: Data?
|
|
||||||
|
|
||||||
init(id: Int, uuid: Data, isPrimaryActivity: Bool, activityType: HKWorkoutActivityType, locationType: HKWorkoutSessionLocationType, swimmingLocationType: HKWorkoutSwimmingLocationType, lapLength: HKQuantity?, startDate: Date, endDate: Date, duration: TimeInterval, metadata: Data?) {
|
|
||||||
self.id = id
|
|
||||||
self.uuid = uuid
|
|
||||||
self.isPrimaryActivity = isPrimaryActivity
|
|
||||||
self.activityType = activityType
|
|
||||||
self.locationType = locationType
|
|
||||||
self.swimmingLocationType = swimmingLocationType
|
|
||||||
self.lapLength = lapLength
|
|
||||||
self.startDate = startDate
|
|
||||||
self.endDate = endDate
|
|
||||||
self.duration = duration
|
|
||||||
self.metadata = metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
func locationSamples(in database: Database) throws -> [LocationSample] {
|
|
||||||
try LocationSample.locationSamples(from: startDate, to: endDate, in: database)
|
|
||||||
}
|
|
||||||
|
|
||||||
func locationSampleCount(in database: Database) throws -> Int {
|
|
||||||
try LocationSample.locationSampleCount(from: startDate, to: endDate, in: database)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sampleCount(in database: Database) throws -> Int {
|
|
||||||
try Sample.sampleCount(from: startDate, to: endDate, in: database)
|
|
||||||
}
|
|
||||||
|
|
||||||
func samples(in database: Database) throws -> [Sample.DataType : [Sample]] {
|
|
||||||
try Sample.samples(from: startDate, to: endDate, in: database).reduce(into: [:]) {
|
|
||||||
$0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension WorkoutActivity: Equatable {
|
|
||||||
|
|
||||||
static func == (lhs: WorkoutActivity, rhs: WorkoutActivity) -> Bool {
|
|
||||||
lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension WorkoutActivity: Comparable {
|
|
||||||
|
|
||||||
static func < (lhs: WorkoutActivity, rhs: WorkoutActivity) -> Bool {
|
|
||||||
lhs.startDate < rhs.startDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension WorkoutActivity: Hashable {
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
}
|
|
@ -60,7 +60,7 @@ private extension WorkoutEventMetadata.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func from(key: String, value: Any) -> Self? {
|
static func from(key: String, value: Any) -> Self? {
|
||||||
if let value = value as? UInt {
|
if let value = value as? UInt64 {
|
||||||
return .with {
|
return .with {
|
||||||
$0.key = key
|
$0.key = key
|
||||||
$0.unsignedValue = UInt64(value)
|
$0.unsignedValue = UInt64(value)
|
||||||
|
@ -20,7 +20,7 @@ extension HealthDatabase {
|
|||||||
|
|
||||||
try Workout.createTable(in: database)
|
try Workout.createTable(in: database)
|
||||||
try HKWorkoutEventTable.create(in: database)
|
try HKWorkoutEventTable.create(in: database)
|
||||||
try WorkoutActivity.createTable(in: database)
|
try WorkoutActivityTable.create(in: database)
|
||||||
try Metadata.createTables(in: database)
|
try Metadata.createTables(in: database)
|
||||||
|
|
||||||
try Workout.mock1.insert(in: database)
|
try Workout.mock1.insert(in: database)
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import HealthKit
|
import HealthKit
|
||||||
|
|
||||||
extension WorkoutActivity {
|
extension HKWorkoutActivity {
|
||||||
|
|
||||||
static var mock1: WorkoutActivity = .init(
|
static var mock1: HKWorkoutActivity = {
|
||||||
id: 744,
|
let configuration = HKWorkoutConfiguration()
|
||||||
uuid: Data(hex: "0e0019a803d541e7b240feb7a360911a")!,
|
configuration.activityType = .init(rawValue: 24)!
|
||||||
isPrimaryActivity: true,
|
configuration.locationType = .init(rawValue: 3)!
|
||||||
activityType: .init(rawValue: 24)!,
|
configuration.swimmingLocationType = .init(rawValue: 0)!
|
||||||
locationType: .init(rawValue: 3)!,
|
configuration.lapLength = nil
|
||||||
swimmingLocationType: .init(rawValue: 0)!,
|
|
||||||
lapLength: nil,
|
let metadata: [String: Any] = [ HKMetadataKeyExternalUUID : "0E0019A803D541E7B240FEB7A360911A"]
|
||||||
startDate: .init(timeIntervalSinceReferenceDate: 702107518.84307),
|
return .init(
|
||||||
endDate: .init(timeIntervalSinceReferenceDate: 702143189.432644),
|
workoutConfiguration: configuration,
|
||||||
duration: 27405.1830769777,
|
start: .init(timeIntervalSinceReferenceDate: 702107518.84307),
|
||||||
metadata: nil)
|
end: .init(timeIntervalSinceReferenceDate: 702143189.432644),
|
||||||
|
metadata: metadata)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ struct WorkoutDetailView: View {
|
|||||||
Section("Activities") {
|
Section("Activities") {
|
||||||
ForEach(workout.activities, id: \.startDate) { activity in
|
ForEach(workout.activities, id: \.startDate) { activity in
|
||||||
NavigationLink(value: activity) {
|
NavigationLink(value: activity) {
|
||||||
DetailRow(activity.activityType.description,
|
DetailRow(activity.workoutConfiguration.activityType.description,
|
||||||
date: activity.startDate)
|
date: activity.startDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(workout.typeString)
|
.navigationTitle(workout.typeString)
|
||||||
.navigationDestination(for: WorkoutActivity.self) { activity in
|
.navigationDestination(for: HKWorkoutActivity.self) { activity in
|
||||||
ActivityDetailView(activity: activity)
|
ActivityDetailView(activity: activity)
|
||||||
.environmentObject(database)
|
.environmentObject(database)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user