Improve sample view
This commit is contained in:
parent
218705a4d2
commit
ac96e6d4a5
@ -39,6 +39,8 @@
|
|||||||
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; };
|
E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; };
|
||||||
E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */; };
|
E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */; };
|
||||||
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* Metadata.swift */; };
|
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* Metadata.swift */; };
|
||||||
|
E201EC7D2B62930E005B83D3 /* Sample+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */; };
|
||||||
|
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; };
|
||||||
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSample.swift */; };
|
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSample.swift */; };
|
||||||
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; };
|
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; };
|
||||||
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; };
|
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; };
|
||||||
@ -47,6 +49,7 @@
|
|||||||
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6852B5FBF0B003A8873 /* Sample.swift */; };
|
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6852B5FBF0B003A8873 /* Sample.swift */; };
|
||||||
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */; };
|
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */; };
|
||||||
E27BC68A2B5FC255003A8873 /* Sample+Unit.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* Sample+Unit.swift */; };
|
E27BC68A2B5FC255003A8873 /* Sample+Unit.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6892B5FC255003A8873 /* Sample+Unit.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 /* WorkoutEvent+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC68D2B5FCBD5003A8873 /* WorkoutEvent+SQLite.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 */; };
|
||||||
@ -85,6 +88,8 @@
|
|||||||
E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = "<group>"; };
|
E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = "<group>"; };
|
||||||
E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetadataKey+SQLite.swift"; sourceTree = "<group>"; };
|
E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetadataKey+SQLite.swift"; sourceTree = "<group>"; };
|
||||||
E201EC7A2B6275CA005B83D3 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
|
E201EC7A2B6275CA005B83D3 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
|
||||||
|
E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+SQLite.swift"; sourceTree = "<group>"; };
|
||||||
|
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = "<group>"; };
|
||||||
E27BC6792B5D99AC003A8873 /* LocationSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSample.swift; sourceTree = "<group>"; };
|
E27BC6792B5D99AC003A8873 /* LocationSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSample.swift; sourceTree = "<group>"; };
|
||||||
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; };
|
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = "<group>"; };
|
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = "<group>"; };
|
||||||
@ -93,6 +98,7 @@
|
|||||||
E27BC6852B5FBF0B003A8873 /* Sample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sample.swift; sourceTree = "<group>"; };
|
E27BC6852B5FBF0B003A8873 /* Sample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sample.swift; sourceTree = "<group>"; };
|
||||||
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Quantity.swift"; sourceTree = "<group>"; };
|
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Quantity.swift"; sourceTree = "<group>"; };
|
||||||
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Unit.swift"; sourceTree = "<group>"; };
|
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sample+Unit.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 /* WorkoutEvent+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+SQLite.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>"; };
|
||||||
@ -140,6 +146,8 @@
|
|||||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
||||||
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
||||||
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
||||||
|
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */,
|
||||||
|
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */,
|
||||||
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */,
|
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */,
|
||||||
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */,
|
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */,
|
||||||
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
|
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
|
||||||
@ -194,6 +202,7 @@
|
|||||||
8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */,
|
8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */,
|
||||||
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
|
E27BC6852B5FBF0B003A8873 /* Sample.swift */,
|
||||||
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */,
|
E27BC6872B5FC220003A8873 /* Sample+Quantity.swift */,
|
||||||
|
E201EC7C2B62930E005B83D3 /* Sample+SQLite.swift */,
|
||||||
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */,
|
E27BC6892B5FC255003A8873 /* Sample+Unit.swift */,
|
||||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||||
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
8850027A2B5C35BF00E7D4DB /* Workout+SQLite.swift */,
|
||||||
@ -302,6 +311,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */,
|
||||||
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
|
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
|
||||||
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */,
|
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */,
|
||||||
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
|
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
|
||||||
@ -324,6 +334,7 @@
|
|||||||
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
|
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
|
||||||
8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */,
|
8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */,
|
||||||
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */,
|
E27BC6882B5FC220003A8873 /* Sample+Quantity.swift in Sources */,
|
||||||
|
E201EC7D2B62930E005B83D3 /* Sample+SQLite.swift in Sources */,
|
||||||
8850027B2B5C35BF00E7D4DB /* Workout+SQLite.swift in Sources */,
|
8850027B2B5C35BF00E7D4DB /* Workout+SQLite.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 */,
|
||||||
@ -338,6 +349,7 @@
|
|||||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
||||||
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
|
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
|
||||||
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
|
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
|
||||||
|
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
|
||||||
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
|
E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */,
|
||||||
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */,
|
E27BC68E2B5FCBD5003A8873 /* WorkoutEvent+SQLite.swift in Sources */,
|
||||||
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
||||||
|
@ -9,6 +9,8 @@ struct ActivityDetailView: View {
|
|||||||
|
|
||||||
@State var locations: [LocationSample] = []
|
@State var locations: [LocationSample] = []
|
||||||
|
|
||||||
|
@State var sampleCount: Int = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
DetailRow("UUID", value: activity.uuid)
|
DetailRow("UUID", value: activity.uuid)
|
||||||
@ -28,6 +30,16 @@ struct ActivityDetailView: View {
|
|||||||
} else {
|
} else {
|
||||||
DetailRow("Locations", value: "0")
|
DetailRow("Locations", value: "0")
|
||||||
}
|
}
|
||||||
|
if sampleCount != 0 {
|
||||||
|
NavigationLink {
|
||||||
|
ActivitySamplesView(activity: activity)
|
||||||
|
.environmentObject(database)
|
||||||
|
} label: {
|
||||||
|
DetailRow("Samples", value: "\(sampleCount)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DetailRow("Samples", value: "0")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Activity")
|
.navigationTitle("Activity")
|
||||||
.navigationDestination(for: [LocationSample].self) { locations in
|
.navigationDestination(for: [LocationSample].self) { locations in
|
||||||
@ -41,8 +53,10 @@ struct ActivityDetailView: View {
|
|||||||
do {
|
do {
|
||||||
let samples = try database.locationSamples(for: activity)
|
let samples = try database.locationSamples(for: activity)
|
||||||
.sorted { $0.timestamp }
|
.sorted { $0.timestamp }
|
||||||
|
let sampleCount = try database.sampleCount(for: activity)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.locations = samples
|
self.locations = samples
|
||||||
|
self.sampleCount = sampleCount
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to load location samples for activity: \(error)")
|
print("Failed to load location samples for activity: \(error)")
|
||||||
|
52
HealthImport/ActivitySamplesView.swift
Normal file
52
HealthImport/ActivitySamplesView.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import OrderedCollections
|
||||||
|
|
||||||
|
struct ActivitySamplesView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var database: HealthDatabase
|
||||||
|
|
||||||
|
let activity: WorkoutActivity
|
||||||
|
|
||||||
|
@State var samples: [(type: Sample.DataType, samples: [Sample])] = []
|
||||||
|
|
||||||
|
init(activity: WorkoutActivity) {
|
||||||
|
self.activity = activity
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(samples, id: \.0) { entry in
|
||||||
|
NavigationLink {
|
||||||
|
SampleListView(type: entry.type, samples: entry.samples)
|
||||||
|
} label: {
|
||||||
|
DetailRow(entry.type.description, value: entry.samples.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onAppear(perform: load)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() {
|
||||||
|
Task {
|
||||||
|
self.loadAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadAsync() {
|
||||||
|
do {
|
||||||
|
let samples = try database.samples(for: activity)
|
||||||
|
let ordered = samples
|
||||||
|
.sorted(using: { $0.key.rawValue })
|
||||||
|
.map { (type: $0, samples: $1) }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.samples = ordered
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to load samples: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ActivitySamplesView(activity: .mock1)
|
||||||
|
}
|
@ -43,6 +43,14 @@ final class HealthDatabase: ObservableObject {
|
|||||||
try activity.locationSamples(in: database)
|
try activity.locationSamples(in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func samples(for activity: WorkoutActivity) throws -> [Sample.DataType : [Sample]] {
|
||||||
|
try activity.samples(in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleCount(for activity: WorkoutActivity) throws -> Int {
|
||||||
|
try activity.sampleCount(in: database)
|
||||||
|
}
|
||||||
|
|
||||||
var activities: [WorkoutActivity] {
|
var activities: [WorkoutActivity] {
|
||||||
workouts.map { $0.activities }.joined().sorted()
|
workouts.map { $0.activities }.joined().sorted()
|
||||||
}
|
}
|
||||||
|
@ -9,63 +9,84 @@ extension LocationSample {
|
|||||||
private static let table = Table("location_series_data")
|
private static let table = Table("location_series_data")
|
||||||
|
|
||||||
/// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]`
|
/// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]`
|
||||||
private static let rowSeriesIdentifier = Expression<Int>("series_identifier")
|
private static let columnSeriesIdentifier = Expression<Int>("series_identifier")
|
||||||
|
|
||||||
private static let rowTimestamp = Expression<Double>("timestamp")
|
private static let columnTimestamp = Expression<Double>("timestamp")
|
||||||
|
|
||||||
private static let rowLongitude = Expression<Double>("longitude")
|
private static let columnLongitude = Expression<Double>("longitude")
|
||||||
|
|
||||||
private static let rowLatitude = Expression<Double>("latitude")
|
private static let columnLatitude = Expression<Double>("latitude")
|
||||||
|
|
||||||
private static let rowAltitude = Expression<Double>("altitude")
|
private static let columnAltitude = Expression<Double>("altitude")
|
||||||
|
|
||||||
private static let rowSpeed = Expression<Double>("speed")
|
private static let columnSpeed = Expression<Double>("speed")
|
||||||
|
|
||||||
private static let rowCourse = Expression<Double>("course")
|
private static let columnCourse = Expression<Double>("course")
|
||||||
|
|
||||||
private static let rowHorizontalAccuracy = Expression<Double>("horizontal_accuracy")
|
private static let columnHorizontalAccuracy = Expression<Double>("horizontal_accuracy")
|
||||||
|
|
||||||
private static let rowVerticalAccuracy = Expression<Double>("vertical_accuracy")
|
private static let columnVerticalAccuracy = Expression<Double>("vertical_accuracy")
|
||||||
|
|
||||||
private static let rowSpeedAccuracy = Expression<Double>("speed_accuracy")
|
private static let columnSpeedAccuracy = Expression<Double>("speed_accuracy")
|
||||||
|
|
||||||
private static let rowCourseAccuracy = Expression<Double>("course_accuracy")
|
private static let columnCourseAccuracy = Expression<Double>("course_accuracy")
|
||||||
|
|
||||||
private static let rowSignalEnvironment = Expression<Double>("signal_environment")
|
private static let columnSignalEnvironment = Expression<Double>("signal_environment")
|
||||||
|
|
||||||
static func locationSamples(for seriesId: Int, in database: Database) throws -> [LocationSample] {
|
static func locationSamples(for seriesId: Int, in database: Database) throws -> [LocationSample] {
|
||||||
try database.prepare(table.filter(rowSeriesIdentifier == seriesId)).map(location)
|
try database.prepare(table.filter(columnSeriesIdentifier == seriesId)).map(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func locationSampleCount(for seriesId: Int, in database: Database) throws -> Int {
|
static func locationSampleCount(for seriesId: Int, in database: Database) throws -> Int {
|
||||||
try database.scalar(table.filter(rowSeriesIdentifier == seriesId).count)
|
try database.scalar(table.filter(columnSeriesIdentifier == seriesId).count)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func locationSamples(from start: Date, to end: Date, in database: Database) throws -> [LocationSample] {
|
static func locationSamples(from start: Date, to end: Date, in database: Database) throws -> [LocationSample] {
|
||||||
let startTime = start.timeIntervalSinceReferenceDate
|
let startTime = start.timeIntervalSinceReferenceDate
|
||||||
let endTime = end.timeIntervalSinceReferenceDate
|
let endTime = end.timeIntervalSinceReferenceDate
|
||||||
return try database.prepare(table.filter(rowTimestamp >= startTime && rowTimestamp <= endTime)).map(location)
|
return try database.prepare(table.filter(columnTimestamp >= startTime && columnTimestamp <= endTime)).map(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func locationSampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
|
static func locationSampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
|
||||||
let startTime = start.timeIntervalSinceReferenceDate
|
let startTime = start.timeIntervalSinceReferenceDate
|
||||||
let endTime = end.timeIntervalSinceReferenceDate
|
let endTime = end.timeIntervalSinceReferenceDate
|
||||||
return try database.scalar(table.filter(rowTimestamp >= startTime && rowTimestamp <= endTime).count)
|
return try database.scalar(table.filter(columnTimestamp >= startTime && columnTimestamp <= endTime).count)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func location(row: Row) -> LocationSample {
|
private static func location(row: Row) -> LocationSample {
|
||||||
.init(
|
.init(
|
||||||
coordinate: .init(
|
coordinate: .init(
|
||||||
latitude: row[rowLatitude],
|
latitude: row[columnLatitude],
|
||||||
longitude: row[rowLongitude]),
|
longitude: row[columnLongitude]),
|
||||||
altitude: row[rowAltitude],
|
altitude: row[columnAltitude],
|
||||||
horizontalAccuracy: row[rowHorizontalAccuracy],
|
horizontalAccuracy: row[columnHorizontalAccuracy],
|
||||||
verticalAccuracy: row[rowHorizontalAccuracy],
|
verticalAccuracy: row[columnHorizontalAccuracy],
|
||||||
course: row[rowCourse],
|
course: row[columnCourse],
|
||||||
courseAccuracy: row[rowCourseAccuracy],
|
courseAccuracy: row[columnCourseAccuracy],
|
||||||
speed: row[rowSpeed],
|
speed: row[columnSpeed],
|
||||||
speedAccuracy: row[rowSpeedAccuracy],
|
speedAccuracy: row[columnSpeedAccuracy],
|
||||||
timestamp: .init(timeIntervalSinceReferenceDate: row[rowTimestamp]),
|
timestamp: .init(timeIntervalSinceReferenceDate: row[columnTimestamp]),
|
||||||
sourceInfo: .init())
|
sourceInfo: .init())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func createTable(in database: Connection) throws {
|
||||||
|
try database.execute("CREATE TABLE location_series_data (series_identifier INTEGER NOT NULL REFERENCES data_series(hfd_key) DEFERRABLE INITIALLY DEFERRED, timestamp REAL NOT NULL, longitude REAL NOT NULL, latitude REAL NOT NULL, altitude REAL NOT NULL, speed REAL NOT NULL, course REAL NOT NULL, horizontal_accuracy REAL NOT NULL, vertical_accuracy REAL NOT NULL, speed_accuracy REAL NOT NULL, course_accuracy REAL NOT NULL, signal_environment INTEGER NOT NULL, PRIMARY KEY (series_identifier, timestamp)) WITHOUT ROWID")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func insert(_ sample: LocationSample, in database: Connection, seriesId: Int) throws {
|
||||||
|
try database.run(table.insert(
|
||||||
|
columnSeriesIdentifier <- seriesId,
|
||||||
|
columnTimestamp <- sample.timestamp.timeIntervalSinceReferenceDate,
|
||||||
|
columnLongitude <- sample.coordinate.longitude,
|
||||||
|
columnLatitude <- sample.coordinate.latitude,
|
||||||
|
columnAltitude <- sample.altitude,
|
||||||
|
columnSpeed <- sample.speed,
|
||||||
|
columnCourse <- sample.course,
|
||||||
|
columnHorizontalAccuracy <- sample.horizontalAccuracy,
|
||||||
|
columnHorizontalAccuracy <- sample.verticalAccuracy,
|
||||||
|
columnSpeedAccuracy <- sample.speedAccuracy,
|
||||||
|
columnCourseAccuracy <- sample.courseAccuracy,
|
||||||
|
columnSignalEnvironment <- 1
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
44
HealthImport/Model/Sample+SQLite.swift
Normal file
44
HealthImport/Model/Sample+SQLite.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
import SQLite
|
||||||
|
|
||||||
|
extension Sample {
|
||||||
|
|
||||||
|
private static let table = Table("samples")
|
||||||
|
|
||||||
|
private static let columnDataId = Expression<Int>("data_id")
|
||||||
|
|
||||||
|
// NOTE: Technically optional
|
||||||
|
private static let columnStartDate = Expression<Double>("start_date")
|
||||||
|
|
||||||
|
// NOTE: Technically optional
|
||||||
|
private static let columnEndDate = Expression<Double>("end_date")
|
||||||
|
|
||||||
|
private static let columnDataType = Expression<Int>("data_type")
|
||||||
|
|
||||||
|
static func samples(from start: Date, to end: Date, in database: Database) throws -> [Sample] {
|
||||||
|
let start = start.timeIntervalSinceReferenceDate
|
||||||
|
let end = end.timeIntervalSinceReferenceDate
|
||||||
|
return try database.prepare(table.filter(columnStartDate >= start && columnEndDate <= end)).compactMap { (row: Row) -> Sample? in
|
||||||
|
let dataId = row[columnDataId]
|
||||||
|
guard let quantity = try Sample.quantity(for: dataId, in: database) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit = try quantity.unit.map { try Sample.unit(for: $0, in: database) }
|
||||||
|
|
||||||
|
return Sample(
|
||||||
|
startDate: Date(timeIntervalSinceReferenceDate: row[columnStartDate]),
|
||||||
|
endDate: Date(timeIntervalSinceReferenceDate: row[columnEndDate]),
|
||||||
|
dataType: .init(rawValue: row[columnDataType]),
|
||||||
|
quantity: quantity.quantity,
|
||||||
|
originalQuantity: quantity.original,
|
||||||
|
originalUnit: unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
|
||||||
|
let start = start.timeIntervalSinceReferenceDate
|
||||||
|
let end = end.timeIntervalSinceReferenceDate
|
||||||
|
return try database.scalar(table.filter(columnStartDate >= start && columnEndDate <= end).count)
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SQLite
|
|
||||||
|
|
||||||
enum SampleDataType: RawRepresentable {
|
|
||||||
|
|
||||||
case unknown(Int)
|
|
||||||
|
|
||||||
init(rawValue: Int) {
|
|
||||||
switch rawValue {
|
|
||||||
|
|
||||||
default:
|
|
||||||
self = .unknown(rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawValue: Int {
|
|
||||||
switch self {
|
|
||||||
case .unknown(let value):
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SampleDataType: Equatable {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SampleDataType: Hashable {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Sample {
|
struct Sample {
|
||||||
|
|
||||||
@ -35,53 +6,117 @@ struct Sample {
|
|||||||
|
|
||||||
let endDate: Date
|
let endDate: Date
|
||||||
|
|
||||||
let dataType: SampleDataType
|
let dataType: DataType
|
||||||
|
|
||||||
let quantity: Double
|
let quantity: Double
|
||||||
|
|
||||||
let originalQuantity: Double?
|
let originalQuantity: Double?
|
||||||
|
|
||||||
let originalUnit: String?
|
let originalUnit: String?
|
||||||
|
|
||||||
|
var duration: TimeInterval {
|
||||||
|
endDate.timeIntervalSince(startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalQuantityText: String {
|
||||||
|
guard let originalQuantity, let originalUnit else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return " (\(originalQuantity) \(originalUnit))"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sample: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
"\(startDate.timeAndDateText) (\(Int(duration)) s) \(quantity)\(originalQuantityText)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Sample {
|
extension Sample {
|
||||||
|
|
||||||
private static let table = Table("samples")
|
enum DataType: RawRepresentable {
|
||||||
|
|
||||||
private static let columnDataId = Expression<Int>("data_id")
|
case weight // 3
|
||||||
|
case heartRate // 5
|
||||||
|
case stepCount // 7
|
||||||
|
case distance // 8
|
||||||
|
case restingEnergy // 9
|
||||||
|
case activeEnergy // 10
|
||||||
|
case flightsClimed // 12
|
||||||
|
case weeklyCalorieGoal // 67
|
||||||
|
case watchOn // 70
|
||||||
|
case standMinutes // 75
|
||||||
|
case activity // 76
|
||||||
|
case workout // 79
|
||||||
|
|
||||||
// NOTE: Technically optional
|
case unknown(Int)
|
||||||
private static let columnStartDate = Expression<Double>("start_date")
|
|
||||||
|
|
||||||
// NOTE: Technically optional
|
init(rawValue: Int) {
|
||||||
private static let columnEndDate = Expression<Double>("end_date")
|
switch rawValue {
|
||||||
|
case 3: self = .weight
|
||||||
private static let columnDataType = Expression<Int>("data_type")
|
case 5: self = .heartRate
|
||||||
|
case 7: self = .stepCount
|
||||||
static func samples(from start: Date, to end: Date, in database: Database) throws -> [Sample] {
|
case 8: self = .distance
|
||||||
let start = start.timeIntervalSinceReferenceDate
|
case 9: self = .restingEnergy
|
||||||
let end = end.timeIntervalSinceReferenceDate
|
case 10: self = .activeEnergy
|
||||||
return try database.prepare(table.filter(columnStartDate >= start && columnEndDate <= end)).compactMap { (row: Row) -> Sample? in
|
case 12: self = .flightsClimed
|
||||||
let dataId = row[columnDataId]
|
case 67: self = .weeklyCalorieGoal
|
||||||
guard let quantity = try Sample.quantity(for: dataId, in: database) else {
|
case 70: self = .watchOn
|
||||||
return nil
|
case 75: self = .standMinutes
|
||||||
}
|
case 76: self = .activity
|
||||||
|
case 79: self = .workout
|
||||||
let unit = try quantity.unit.map { try Sample.unit(for: $0, in: database) }
|
default:
|
||||||
|
self = .unknown(rawValue)
|
||||||
return Sample(
|
|
||||||
startDate: Date(timeIntervalSinceReferenceDate: row[columnStartDate]),
|
|
||||||
endDate: Date(timeIntervalSinceReferenceDate: row[columnEndDate]),
|
|
||||||
dataType: .init(rawValue: row[columnDataType]),
|
|
||||||
quantity: quantity.quantity,
|
|
||||||
originalQuantity: quantity.original,
|
|
||||||
originalUnit: unit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func sampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
|
var rawValue: Int {
|
||||||
let start = start.timeIntervalSinceReferenceDate
|
switch self {
|
||||||
let end = end.timeIntervalSinceReferenceDate
|
case .stepCount: return 7
|
||||||
return try database.scalar(table.filter(columnStartDate >= start && columnEndDate <= end).count)
|
case .weight: return 3
|
||||||
|
case .heartRate: return 5
|
||||||
|
case .distance: return 8
|
||||||
|
case .restingEnergy: return 9
|
||||||
|
case .activeEnergy: return 10
|
||||||
|
case .flightsClimed: return 12
|
||||||
|
case .weeklyCalorieGoal: return 67
|
||||||
|
case .watchOn: return 70
|
||||||
|
case .standMinutes: return 75
|
||||||
|
case .activity: return 76
|
||||||
|
case .workout: return 79
|
||||||
|
case .unknown(let value): return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sample.DataType: Equatable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sample.DataType: Hashable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sample.DataType: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .stepCount: return "Step Count"
|
||||||
|
case .weight: return "Weight"
|
||||||
|
case .heartRate: return "Heart Rate"
|
||||||
|
case .distance: return "Distance"
|
||||||
|
case .restingEnergy: return "Resting Energy"
|
||||||
|
case .activeEnergy: return "Active Energy"
|
||||||
|
case .flightsClimed: return "Flights Climbed"
|
||||||
|
case .weeklyCalorieGoal: return "Weekly Calorie Goal"
|
||||||
|
case .watchOn: return "Watch On"
|
||||||
|
case .standMinutes: return "Stand Minutes"
|
||||||
|
case .activity: return "Activity"
|
||||||
|
case .workout: return "Workout"
|
||||||
|
case .unknown(let int): return "Unknown(\(int))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,9 +52,9 @@ struct WorkoutActivity {
|
|||||||
try Sample.sampleCount(from: startDate, to: endDate, in: database)
|
try Sample.sampleCount(from: startDate, to: endDate, in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func samples(in database: Database) throws -> [SampleDataType : Sample] {
|
func samples(in database: Database) throws -> [Sample.DataType : [Sample]] {
|
||||||
try Sample.samples(from: startDate, to: endDate, in: database).reduce(into: [:]) {
|
try Sample.samples(from: startDate, to: endDate, in: database).reduce(into: [:]) {
|
||||||
$0[$1.dataType] = $1
|
$0[$1.dataType] = ($0[$1.dataType] ?? []) + [$1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
HealthImport/SampleListView.swift
Normal file
29
HealthImport/SampleListView.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SampleListView: View {
|
||||||
|
|
||||||
|
let type: Sample.DataType
|
||||||
|
|
||||||
|
let samples: [Sample]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(samples) { sample in
|
||||||
|
DetailRow("", value: sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(type.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
#Preview {
|
||||||
|
SampleListView()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
extension Sample: Identifiable {
|
||||||
|
|
||||||
|
var id: Date {
|
||||||
|
startDate
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user