Extract metadata
This commit is contained in:
parent
8ace8e9319
commit
0088e5df2e
@ -31,6 +31,12 @@
|
||||
885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */; };
|
||||
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */; };
|
||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029C2B5D197300E7D4DB /* EventDetailView.swift */; };
|
||||
8850029F2B5D1C7000E7D4DB /* DBMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029E2B5D1C7000E7D4DB /* DBMetadata.swift */; };
|
||||
885002A12B5D1E7400E7D4DB /* DBMetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002A02B5D1E7400E7D4DB /* DBMetadataKey.swift */; };
|
||||
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002A22B5D217600E7D4DB /* MetadataValue.swift */; };
|
||||
885002A62B5D296700E7D4DB /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A52B5D296700E7D4DB /* Collections */; };
|
||||
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A72B5D296700E7D4DB /* DequeModule */; };
|
||||
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A92B5D296700E7D4DB /* OrderedCollections */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -58,6 +64,9 @@
|
||||
885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSwimmingLocationType+Extensions.swift"; sourceTree = "<group>"; };
|
||||
8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = "<group>"; };
|
||||
8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
|
||||
8850029E2B5D1C7000E7D4DB /* DBMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMetadata.swift; sourceTree = "<group>"; };
|
||||
885002A02B5D1E7400E7D4DB /* DBMetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMetadataKey.swift; sourceTree = "<group>"; };
|
||||
885002A22B5D217600E7D4DB /* MetadataValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValue.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -65,7 +74,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
885002A62B5D296700E7D4DB /* Collections in Frameworks */,
|
||||
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
|
||||
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */,
|
||||
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -130,6 +142,8 @@
|
||||
8850027A2B5C35BF00E7D4DB /* DBWorkout.swift */,
|
||||
8850027C2B5C360300E7D4DB /* DBWorkoutEvent.swift */,
|
||||
885002882B5C873C00E7D4DB /* DBWorkoutActivity.swift */,
|
||||
8850029E2B5D1C7000E7D4DB /* DBMetadata.swift */,
|
||||
885002A02B5D1E7400E7D4DB /* DBMetadataKey.swift */,
|
||||
);
|
||||
path = "Database Entries";
|
||||
sourceTree = "<group>";
|
||||
@ -140,6 +154,7 @@
|
||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */,
|
||||
8850028A2B5C896C00E7D4DB /* WorkoutActivity.swift */,
|
||||
885002A22B5D217600E7D4DB /* MetadataValue.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -176,6 +191,9 @@
|
||||
name = HealthImport;
|
||||
packageProductDependencies = (
|
||||
885002762B5C2FC400E7D4DB /* SQLite */,
|
||||
885002A52B5D296700E7D4DB /* Collections */,
|
||||
885002A72B5D296700E7D4DB /* DequeModule */,
|
||||
885002A92B5D296700E7D4DB /* OrderedCollections */,
|
||||
);
|
||||
productName = HealthImport;
|
||||
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
||||
@ -207,6 +225,7 @@
|
||||
mainGroup = 8850024E2B5C273C00E7D4DB;
|
||||
packageReferences = (
|
||||
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||
885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
);
|
||||
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -245,10 +264,13 @@
|
||||
885002852B5C7AD600E7D4DB /* WorkoutEvent.swift in Sources */,
|
||||
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
|
||||
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */,
|
||||
885002A12B5D1E7400E7D4DB /* DBMetadataKey.swift in Sources */,
|
||||
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */,
|
||||
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
|
||||
8850029F2B5D1C7000E7D4DB /* DBMetadata.swift in Sources */,
|
||||
8850027B2B5C35BF00E7D4DB /* DBWorkout.swift in Sources */,
|
||||
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
||||
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
|
||||
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */,
|
||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
||||
@ -470,6 +492,14 @@
|
||||
minimumVersion = 0.14.1;
|
||||
};
|
||||
};
|
||||
885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.6;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -478,6 +508,21 @@
|
||||
package = 885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||
productName = SQLite;
|
||||
};
|
||||
885002A52B5D296700E7D4DB /* Collections */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = Collections;
|
||||
};
|
||||
885002A72B5D296700E7D4DB /* DequeModule */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = DequeModule;
|
||||
};
|
||||
885002A92B5D296700E7D4DB /* OrderedCollections */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = OrderedCollections;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 8850024F2B5C273C00E7D4DB /* Project object */;
|
||||
|
@ -8,6 +8,15 @@
|
||||
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
|
||||
"version" : "0.14.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "d029d9d39c87bed85b1c50adee7c41795261a192",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
57
HealthImport/Database Entries/DBMetadata.swift
Normal file
57
HealthImport/Database Entries/DBMetadata.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct DBMetadata {
|
||||
|
||||
private static let table = Table("metadata_values")
|
||||
|
||||
private static let rowKeyId = Expression<Int?>("key_id")
|
||||
|
||||
private static let rowObjectId = Expression<Int?>("object_id")
|
||||
|
||||
private static let rowValueType = Expression<Int>("value_type")
|
||||
|
||||
private static let rowStringValue = Expression<String?>("string_value")
|
||||
|
||||
private static let rowNumericalValue = Expression<Double?>("numerical_value")
|
||||
|
||||
private static let rowDateValue = Expression<Double?>("date_value")
|
||||
|
||||
private static let rowDataValue = Expression<Data?>("data_value")
|
||||
|
||||
static func readAll(in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table).map(Self.init)
|
||||
}
|
||||
|
||||
static func metadata(for workoutId: Int, in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table.filter(rowObjectId == workoutId)).map(Self.init)
|
||||
}
|
||||
|
||||
let keyId: Int?
|
||||
|
||||
let objectId: Int?
|
||||
|
||||
let valueType: Int
|
||||
|
||||
let string: String?
|
||||
|
||||
let number: Double?
|
||||
|
||||
let date: Double?
|
||||
|
||||
let data: Data?
|
||||
}
|
||||
|
||||
extension DBMetadata {
|
||||
|
||||
init(row: Row) {
|
||||
self.keyId = row[DBMetadata.rowKeyId]
|
||||
self.objectId = row[DBMetadata.rowObjectId]
|
||||
self.valueType = row[DBMetadata.rowValueType]
|
||||
self.string = row[DBMetadata.rowStringValue]
|
||||
self.number = row[DBMetadata.rowNumericalValue]
|
||||
self.date = row[DBMetadata.rowDateValue]
|
||||
self.data = row[DBMetadata.rowDataValue]
|
||||
}
|
||||
}
|
||||
|
21
HealthImport/Database Entries/DBMetadataKey.swift
Normal file
21
HealthImport/Database Entries/DBMetadataKey.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct DBMetadataKey {
|
||||
|
||||
private static let table = Table("metadata_keys")
|
||||
|
||||
private static let rowId = Expression<Int>("ROWID")
|
||||
|
||||
private static let rowKey = Expression<String>("key")
|
||||
|
||||
static func key(for keyId: Int, in database: Connection) throws -> String {
|
||||
try database.prepare(table.filter(rowId == keyId).limit(1)).map { $0[rowKey] }.first!
|
||||
}
|
||||
|
||||
static func readAll(in database: Connection) throws -> [ Int: String] {
|
||||
try database.prepare(table).reduce(into: [:]) { dict, row in
|
||||
dict[row[rowId]] = row[rowKey]
|
||||
}
|
||||
}
|
||||
}
|
@ -21,10 +21,15 @@ final class HealthDatabase: ObservableObject {
|
||||
func readAllWorkouts() {
|
||||
do {
|
||||
let dbWorkouts = try DBWorkout.readAll(in: database)
|
||||
let metadataKeys = try DBMetadataKey.readAll(in: database)
|
||||
let workouts = try dbWorkouts.map { entry in
|
||||
let events = try DBWorkoutEvent.events(for: entry.dataId, in: database)
|
||||
let activities = try DBWorkoutActivity.activities(for: entry.dataId, in: database)
|
||||
return Workout(entry: entry, events: events, activities: activities)
|
||||
let metadata: [String : MetadataValue] = try DBMetadata.metadata(for: entry.dataId, in: database).reduce(into: [:]) { dict, item in
|
||||
let key = metadataKeys[item.keyId!]!
|
||||
dict[key] = MetadataValue(entry: item)
|
||||
}
|
||||
return Workout(entry: entry, events: events, activities: activities, metadata: metadata)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
65
HealthImport/Model/MetadataValue.swift
Normal file
65
HealthImport/Model/MetadataValue.swift
Normal file
@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
|
||||
enum MetadataValue {
|
||||
|
||||
case string(value: String)
|
||||
case number(value: Double)
|
||||
case date(value: Date)
|
||||
case numerical(value: Double, unit: String)
|
||||
case data(value: Data)
|
||||
|
||||
enum ValueType: Int {
|
||||
|
||||
/// Uses only the `string_value` column
|
||||
case string = 0
|
||||
|
||||
/// Uses only the `numerical_value` column
|
||||
case number = 1
|
||||
|
||||
/// Uses only the `date_value` column
|
||||
case date = 2
|
||||
|
||||
/// Uses the `string_value` column for the unit, and the `numerical_value` column for the number
|
||||
case numerical = 3
|
||||
|
||||
/// Uses only the `data_value` column
|
||||
case data = 4
|
||||
}
|
||||
}
|
||||
|
||||
extension MetadataValue {
|
||||
|
||||
init(entry: DBMetadata) {
|
||||
let valueType = ValueType(rawValue: entry.valueType)!
|
||||
switch valueType {
|
||||
case .string:
|
||||
self = .string(value: entry.string!)
|
||||
case .number:
|
||||
self = .number(value: entry.number!)
|
||||
case .date:
|
||||
self = .date(value: .init(timeIntervalSinceReferenceDate: entry.date!))
|
||||
case .numerical:
|
||||
self = .numerical(value: entry.number!, unit: entry.string!)
|
||||
case .data:
|
||||
self = .data(value: entry.data!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MetadataValue: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .string(let value):
|
||||
return value
|
||||
case .number(let value):
|
||||
return "\(value)"
|
||||
case .date(let value):
|
||||
return value.timeAndDateText
|
||||
case .numerical(let value, let unit):
|
||||
return String(format: "%.3f %s", value, unit)
|
||||
case .data(let value):
|
||||
return value.description
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
import Collections
|
||||
|
||||
private let df: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
@ -28,6 +28,8 @@ struct Workout {
|
||||
|
||||
let activities: [WorkoutActivity]
|
||||
|
||||
let metadata: OrderedDictionary<String, MetadataValue>
|
||||
|
||||
var firstActivityDate: Date? {
|
||||
activities.map { $0.startDate }.min()
|
||||
}
|
||||
@ -51,7 +53,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] = []) {
|
||||
init(id: Int, totalDistance: Double? = nil, goalType: Int? = nil, goal: Double? = nil, condenserVersion: Int? = nil, condenserDate: Date? = nil, events: [WorkoutEvent] = [], activities: [WorkoutActivity] = [], metadata: [String : MetadataValue] = [:]) {
|
||||
self.id = id
|
||||
self.totalDistance = totalDistance
|
||||
self.goalType = goalType
|
||||
@ -60,12 +62,13 @@ struct Workout {
|
||||
self.condenserDate = condenserDate
|
||||
self.events = events
|
||||
self.activities = activities
|
||||
self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values)
|
||||
}
|
||||
}
|
||||
|
||||
extension Workout {
|
||||
|
||||
init(entry: DBWorkout, events: [DBWorkoutEvent], activities: [DBWorkoutActivity]) {
|
||||
init(entry: DBWorkout, events: [DBWorkoutEvent], activities: [DBWorkoutActivity], metadata: [String : MetadataValue]) {
|
||||
self.id = entry.dataId
|
||||
self.totalDistance = entry.totalDistance
|
||||
self.goalType = entry.goalType
|
||||
@ -74,6 +77,7 @@ extension Workout {
|
||||
self.condenserDate = entry.condenserDate.map { Date(timeIntervalSinceReferenceDate: $0) }
|
||||
self.events = events.map(WorkoutEvent.init)
|
||||
self.activities = activities.map(WorkoutActivity.init)
|
||||
self.metadata = .init(uniqueKeys: metadata.keys, values: metadata.values)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import Collections
|
||||
|
||||
struct WorkoutDetailView: View {
|
||||
|
||||
@ -37,6 +38,11 @@ struct WorkoutDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !workout.metadata.isEmpty {
|
||||
ForEach(workout.metadata.elements, id:\.key) { (key, value) in
|
||||
DetailRow(key, value: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(workout.typeString)
|
||||
.navigationDestination(for: WorkoutActivity.self) { activity in
|
||||
@ -81,3 +87,8 @@ struct WorkoutDetailView: View {
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
extension String: Identifiable {
|
||||
|
||||
public var id: Self { self }
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user