Switch to HKWorkoutEvent

This commit is contained in:
Christoph Hagen
2024-02-01 15:50:28 +01:00
parent dbe088a402
commit c36ee29afb
13 changed files with 371 additions and 177 deletions

View File

@@ -1,20 +1,21 @@
import SwiftUI
import HealthKit
struct EventDetailView: View {
let event: WorkoutEvent
let event: HKWorkoutEvent
var metadata: [(key: String, value: Any)] {
event.metadata.sorted { $0.key < $1.key }
event.metadata?.sorted { $0.key < $1.key } ?? []
}
var body: some View {
List {
DetailRow("Date", date: event.date)
DetailRow("Date", date: event.dateInterval.start)
DetailRow("Type", value: event.type)
DetailRow("Duration", duration: event.duration)
DetailRow("Session UUID", value: event.sessionUUID)
DetailRow("Error", value: event.error)
DetailRow("Duration", duration: event.dateInterval.duration)
//DetailRow("Session UUID", value: event.sessionUUID)
//DetailRow("Error", value: event.error)
Section("Metadata") {
ForEach(metadata, id: \.key) { (key, value) in
DetailRow(key, value: "\(value)")

View File

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

View File

@@ -1,5 +1,6 @@
import Foundation
import SQLite
import HealthKit
extension Workout {
@@ -29,7 +30,7 @@ extension Workout {
return try database.prepare(table).map { row in
let id = row[columnDataId]
let events = try WorkoutEvent.events(for: id, in: database)
let events = try HKWorkoutEventTable.events(for: id, in: database)
let activities = try WorkoutActivity.activities(for: id, in: database)
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
return .init(

View File

@@ -1,5 +1,6 @@
import Foundation
import Collections
import HealthKit
private let df: DateFormatter = {
let df = DateFormatter()
@@ -22,8 +23,8 @@ struct Workout {
let condenserDate: Date?
let events: [WorkoutEvent]
let events: [HKWorkoutEvent]
let activities: [WorkoutActivity]
let metadata: OrderedDictionary<Metadata.Key, Metadata.Value>
@@ -33,7 +34,7 @@ struct Workout {
}
var firstEventDate: Date? {
events.map { $0.date }.min()
events.map { $0.dateInterval.start }.min()
}
var firstAvailableDate: Date? {
@@ -51,7 +52,7 @@ struct Workout {
activities.first?.activityType.description ?? "Unknown activity"
}
init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [WorkoutEvent] = [], 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: [WorkoutActivity] = [], metadata: [Metadata.Key : Metadata.Value] = [:]) {
self.id = id
self.totalDistance = totalDistance
self.goal = .init(goalType: goalType, goal: goal)

View File

@@ -1,7 +1,8 @@
import Foundation
import SQLite
import HealthKit
extension WorkoutEvent {
enum HKWorkoutEventTable {
private static let table = Table("workout_events")
@@ -29,22 +30,22 @@ extension WorkoutEvent {
// error BLOB
private static let columnError = Expression<Data?>("error")
static func readAll(in database: Connection) throws -> [Self] {
try database.prepare(table).map(from)
static func readAll(in database: Connection) throws -> [HKWorkoutEvent] {
try database.prepare(table).map(event)
}
static func events(for workoutId: Int, in database: Connection) throws -> [Self] {
try database.prepare(table.filter(columnOwnerId == workoutId)).map(from)
static func events(for workoutId: Int, in database: Connection) throws -> [HKWorkoutEvent] {
try database.prepare(table.filter(columnOwnerId == workoutId)).map(event)
}
private static func from(row: Row) -> WorkoutEvent {
.init(
date: Date(timeIntervalSinceReferenceDate: row[columnDate]),
type: .init(rawValue: row[columnType])!,
duration: row[columnDuration],
metadata: metadata(row[columnMetadata]),
sessionUUID: row[columnSessionUUID],
error: row[columnError])
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] {
@@ -54,7 +55,7 @@ extension WorkoutEvent {
return decode(metadata: data)
}
static func createTable(in database: Database) throws {
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)
@@ -68,19 +69,22 @@ extension WorkoutEvent {
})
}
func insert(in database: Database, dataId: Int) throws {
try WorkoutEvent.insert(self, dataId: dataId, in: database)
}
private static func insert(_ element: WorkoutEvent, dataId: Int, in database: Database) throws {
static func insert(_ element: HKWorkoutEvent, dataId: Int, in database: Database) throws {
try database.run(table.insert(
columnOwnerId <- dataId,
columnDate <- element.date.timeIntervalSinceReferenceDate,
columnDate <- element.dateInterval.start.timeIntervalSinceReferenceDate,
columnType <- element.type.rawValue,
columnDuration <- element.duration,
columnMetadata <- encode(metadata: element.metadata),
columnSessionUUID <- element.sessionUUID,
columnError <- element.error)
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)
}
}

View File

@@ -1,59 +1,20 @@
import Foundation
import HealthKit
import BinaryCodable
import SwiftProtobuf
struct WorkoutEvent {
let date: Date
let type: HKWorkoutEventType
let duration: TimeInterval
let metadata: [String : Any]
let sessionUUID: Data?
let error: Data?
}
extension HKWorkoutEvent: Identifiable {
extension WorkoutEvent: Equatable {
static func == (lhs: WorkoutEvent, rhs: WorkoutEvent) -> Bool {
lhs.date == rhs.date && lhs.type == rhs.type && lhs.duration == rhs.duration
public var id: Double {
dateInterval.start.timeIntervalSinceReferenceDate * Double(type.rawValue) * dateInterval.duration
}
}
extension WorkoutEvent: Comparable {
static func < (lhs: WorkoutEvent, rhs: WorkoutEvent) -> Bool {
lhs.date < rhs.date
}
}
extension WorkoutEvent: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(date)
hasher.combine(type.rawValue)
hasher.combine(duration)
}
}
extension WorkoutEvent: Identifiable {
var id: Double {
date.timeIntervalSinceReferenceDate * Double(type.rawValue) * duration
}
}
extension WorkoutEvent {
extension HKWorkoutEventTable {
static func decode(metadata data: Data) -> [String : Any] {
let metadata: WorkoutEventMetadata
do {
metadata = try ProtobufDecoder.decode(WorkoutEventMetadata.self, from: data)
metadata = try WorkoutEventMetadata(serializedData: data)
} catch {
print("Failed to decode event metadata: \(error)")
print(data.hex)
@@ -71,14 +32,14 @@ extension WorkoutEvent {
}
static func encode(metadata: [String : Any]) -> Data? {
let wrapper = WorkoutEventMetadata(elements: metadata.compactMap {
.init(key: $0.key, value: $0.value)
})
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 ProtobufEncoder().encode(wrapper)
return try wrapper.serializedData()
} catch {
print("Failed to encode event metadata: \(error)")
return nil
@@ -86,90 +47,49 @@ extension WorkoutEvent {
}
}
private struct WorkoutEventMetadata {
let elements: [Element]
}
extension WorkoutEventMetadata.Element {
private extension WorkoutEventMetadata.Element {
var value: Any? {
if let unsignedValue {
if hasUnsignedValue {
return unsignedValue
}
if let quantity {
if hasQuantity {
return HKQuantity(unit: .init(from: quantity.unit), doubleValue: quantity.value)
}
return UInt(0)
}
init?(key: String, value: Any) {
self.key = key
static func from(key: String, value: Any) -> Self? {
if let value = value as? UInt {
self.unsignedValue = value
self.quantity = nil
return
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
}
self.unsignedValue = nil
let number: Double
let unit: String
if value.is(compatibleWith: .meter()) {
self.quantity = .init(value: value.doubleValue(for: .meter()), unit: "m")
number = value.doubleValue(for: .meter())
unit = "m"
} else if value.is(compatibleWith: .second()) {
self.quantity = .init(value: value.doubleValue(for: .second()), unit: "s")
number = value.doubleValue(for: .second())
unit = "s"
} else {
print("Unhandled quantity type for metadata key \(key): \(value)")
return nil
}
}
}
extension WorkoutEventMetadata {
struct Element {
let key: String
let unsignedValue: UInt?
let quantity: Quantity?
}
}
extension WorkoutEventMetadata.Element {
struct Quantity {
let value: Double
let unit: String
}
}
extension WorkoutEventMetadata: Codable {
enum CodingKeys: Int, CodingKey {
case elements = 1
}
}
extension WorkoutEventMetadata.Element: Codable {
enum CodingKeys: Int, CodingKey {
case key = 1
case unsignedValue = 4
case quantity = 6
}
}
extension WorkoutEventMetadata.Element.Quantity: Codable {
enum CodingKeys: Int, CodingKey {
case value = 1
case unit = 2
return .with { el in
el.key = key
el.quantity = .with {
$0.value = number
$0.unit = unit
}
}
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
import SQLite
import HealthKit
extension HealthDatabase {
@@ -18,7 +19,7 @@ extension HealthDatabase {
let database = try Connection(.inMemory)
try Workout.createTable(in: database)
try WorkoutEvent.createTable(in: database)
try HKWorkoutEventTable.create(in: database)
try WorkoutActivity.createTable(in: database)
try Metadata.createTables(in: database)

View File

@@ -1,4 +1,5 @@
import Foundation
import HealthKit
extension Workout {
@@ -9,7 +10,7 @@ extension Workout {
goal: 19800.0,
condenserVersion: 3,
condenserDate: Date(timeIntervalSinceReferenceDate: 716801471.790011),
events: WorkoutEvent.mock1,
events: HKWorkoutEvent.mock1,
activities: [.mock1],
metadata: Metadata.mock1)
}

View File

@@ -1,34 +1,26 @@
import Foundation
import HealthKit
extension WorkoutEvent {
extension HKWorkoutEvent {
static var mock1: [WorkoutEvent] {
static var mock1: [HKWorkoutEvent] {
[
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
type: .init(rawValue: 7)!,
duration: 1114.56374406815,
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event1Metadata)!),
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702107518.84307),
type: .init(rawValue: 7)!,
duration: 1972.17168283463,
metadata: WorkoutEvent.decode(metadata: .init(hex: mock1Event2Metadata)!),
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702112942.707113),
type: .init(rawValue: 1)!,
duration: 0.0,
metadata: [:],
sessionUUID: nil,
error: nil),
.init(date: .init(timeIntervalSinceReferenceDate: 702113161.221132),
type: .init(rawValue: 2)!,
duration: 0.0,
metadata: [:],
sessionUUID: nil,
error: nil),
.init(type: .init(rawValue: 7)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702107518.84307),
duration: 1114.56374406815),
metadata: HKWorkoutEventTable.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)!)),
.init(type: .init(rawValue: 1)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702112942.707113),
duration: 0.0),
metadata: HKWorkoutEventTable.decode(metadata: .init(hex: mock1Event2Metadata)!)),
.init(type: .init(rawValue: 2)!,
dateInterval: .init(start: Date(timeIntervalSinceReferenceDate: 702113161.221132),
duration: 0.0),
metadata: [:])
]
}
}

View File

@@ -1,5 +1,6 @@
import SwiftUI
import Collections
import HealthKit
struct WorkoutDetailView: View {
@@ -32,7 +33,7 @@ struct WorkoutDetailView: View {
Section("Events") {
ForEach(workout.events) { event in
NavigationLink(value: event) {
DetailRow(event.type.description, date: event.date)
DetailRow(event.type.description, date: event.dateInterval.start)
}
}
}
@@ -50,7 +51,7 @@ struct WorkoutDetailView: View {
ActivityDetailView(activity: activity)
.environmentObject(database)
}
.navigationDestination(for: WorkoutEvent.self) { event in
.navigationDestination(for: HKWorkoutEvent.self) { event in
EventDetailView(event: event)
}
}