Read workouts, events, activities
This commit is contained in:
parent
b75e0afe4a
commit
8ace8e9319
@ -11,6 +11,26 @@
|
||||
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850025C2B5C273C00E7D4DB /* ContentView.swift */; };
|
||||
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; };
|
||||
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 885002612B5C273E00E7D4DB /* Preview Assets.xcassets */; };
|
||||
8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */; };
|
||||
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002702B5C299900E7D4DB /* HealthDatabase.swift */; };
|
||||
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 885002762B5C2FC400E7D4DB /* SQLite */; };
|
||||
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002782B5C320400E7D4DB /* Optional+Extensions.swift */; };
|
||||
8850027B2B5C35BF00E7D4DB /* DBWorkout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027A2B5C35BF00E7D4DB /* DBWorkout.swift */; };
|
||||
8850027D2B5C360300E7D4DB /* DBWorkoutEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027C2B5C360300E7D4DB /* DBWorkoutEvent.swift */; };
|
||||
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850027E2B5C36A700E7D4DB /* Workout.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 */; };
|
||||
885002892B5C873C00E7D4DB /* DBWorkoutActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002882B5C873C00E7D4DB /* DBWorkoutActivity.swift */; };
|
||||
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850028A2B5C896C00E7D4DB /* WorkoutActivity.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 */; };
|
||||
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */; };
|
||||
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002922B5D129300E7D4DB /* ActivityDetailView.swift */; };
|
||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002942B5D147100E7D4DB /* DetailRow.swift */; };
|
||||
885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -19,6 +39,25 @@
|
||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
8850025E2B5C273E00E7D4DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
885002612B5C273E00E7D4DB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthdb_secure.sqlite; sourceTree = "<group>"; };
|
||||
885002702B5C299900E7D4DB /* HealthDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthDatabase.swift; sourceTree = "<group>"; };
|
||||
885002782B5C320400E7D4DB /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
|
||||
8850027A2B5C35BF00E7D4DB /* DBWorkout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWorkout.swift; sourceTree = "<group>"; };
|
||||
8850027C2B5C360300E7D4DB /* DBWorkoutEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWorkoutEvent.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>"; };
|
||||
885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivityType+Extensions.swift"; sourceTree = "<group>"; };
|
||||
885002882B5C873C00E7D4DB /* DBWorkoutActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWorkoutActivity.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>"; };
|
||||
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>"; };
|
||||
885002922B5D129300E7D4DB /* ActivityDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityDetailView.swift; sourceTree = "<group>"; };
|
||||
885002942B5D147100E7D4DB /* DetailRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailRow.swift; sourceTree = "<group>"; };
|
||||
885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSessionLocationType+Extensions.swift"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -26,6 +65,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -51,10 +91,19 @@
|
||||
885002592B5C273C00E7D4DB /* HealthImport */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8850026A2B5C276B00E7D4DB /* Resources */,
|
||||
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
||||
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
||||
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
||||
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
|
||||
885002942B5D147100E7D4DB /* DetailRow.swift */,
|
||||
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
|
||||
885002602B5C273E00E7D4DB /* Preview Content */,
|
||||
885002702B5C299900E7D4DB /* HealthDatabase.swift */,
|
||||
885002802B5C37A800E7D4DB /* Database Entries */,
|
||||
885002812B5C37B700E7D4DB /* Model */,
|
||||
885002832B5C37C600E7D4DB /* Support */,
|
||||
);
|
||||
path = HealthImport;
|
||||
sourceTree = "<group>";
|
||||
@ -67,6 +116,48 @@
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8850026A2B5C276B00E7D4DB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8850026B2B5C278600E7D4DB /* healthdb_secure.sqlite */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
885002802B5C37A800E7D4DB /* Database Entries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8850027A2B5C35BF00E7D4DB /* DBWorkout.swift */,
|
||||
8850027C2B5C360300E7D4DB /* DBWorkoutEvent.swift */,
|
||||
885002882B5C873C00E7D4DB /* DBWorkoutActivity.swift */,
|
||||
);
|
||||
path = "Database Entries";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
885002812B5C37B700E7D4DB /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8850027E2B5C36A700E7D4DB /* Workout.swift */,
|
||||
885002842B5C7AD600E7D4DB /* WorkoutEvent.swift */,
|
||||
8850028A2B5C896C00E7D4DB /* WorkoutActivity.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
885002832B5C37C600E7D4DB /* Support */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8850028E2B5D0EAF00E7D4DB /* Date+Extensions.swift */,
|
||||
885002782B5C320400E7D4DB /* Optional+Extensions.swift */,
|
||||
885002862B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift */,
|
||||
885002902B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift */,
|
||||
885002962B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift */,
|
||||
885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */,
|
||||
8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */,
|
||||
);
|
||||
path = Support;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -83,6 +174,9 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = HealthImport;
|
||||
packageProductDependencies = (
|
||||
885002762B5C2FC400E7D4DB /* SQLite */,
|
||||
);
|
||||
productName = HealthImport;
|
||||
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -111,6 +205,9 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 8850024E2B5C273C00E7D4DB;
|
||||
packageReferences = (
|
||||
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||
);
|
||||
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -125,6 +222,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */,
|
||||
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */,
|
||||
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */,
|
||||
);
|
||||
@ -137,7 +235,25 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */,
|
||||
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
|
||||
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */,
|
||||
885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */,
|
||||
885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */,
|
||||
885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */,
|
||||
8850028F2B5D0EAF00E7D4DB /* Date+Extensions.swift in Sources */,
|
||||
885002852B5C7AD600E7D4DB /* WorkoutEvent.swift in Sources */,
|
||||
885002912B5D0F9200E7D4DB /* HKWorkoutEventType+Extensions.swift in Sources */,
|
||||
8850027F2B5C36A700E7D4DB /* Workout.swift in Sources */,
|
||||
885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */,
|
||||
885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */,
|
||||
8850027B2B5C35BF00E7D4DB /* DBWorkout.swift in Sources */,
|
||||
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
||||
8850028B2B5C896C00E7D4DB /* WorkoutActivity.swift in Sources */,
|
||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
||||
8850027D2B5C360300E7D4DB /* DBWorkoutEvent.swift in Sources */,
|
||||
885002892B5C873C00E7D4DB /* DBWorkoutActivity.swift in Sources */,
|
||||
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -344,6 +460,25 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.14.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
885002762B5C2FC400E7D4DB /* SQLite */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||
productName = SQLite;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 8850024F2B5C273C00E7D4DB /* Project object */;
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "sqlite.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/stephencelis/SQLite.swift",
|
||||
"state" : {
|
||||
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
|
||||
"version" : "0.14.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
@ -9,6 +9,27 @@
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground) 1.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground) 2.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground).xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
39
HealthImport/ActivityDetailView.swift
Normal file
39
HealthImport/ActivityDetailView.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityDetailView: View {
|
||||
|
||||
let activity: WorkoutActivity
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
DetailRow("UUID", value: activity.uuid)
|
||||
DetailRow("Primary Activity", value: activity.isPrimaryActivity)
|
||||
DetailRow("Activity", value: activity.activityType)
|
||||
DetailRow("Location", value: activity.locationType)
|
||||
DetailRow("Swimming Location", value: activity.swimmingLocationType)
|
||||
DetailRow("Lap Length", value: activity.lapLength)
|
||||
DetailRow("Start", date: activity.startDate)
|
||||
DetailRow("End", date: activity.endDate)
|
||||
DetailRow("Duration", duration: activity.duration)
|
||||
DetailRow("Metadata", value: activity.metadata)
|
||||
}
|
||||
.navigationTitle("Activity")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
ActivityDetailView(activity: .init(
|
||||
uuid: .init(repeating: 42, count: 3),
|
||||
isPrimaryActivity: true,
|
||||
activityType: .running,
|
||||
locationType: .outdoor,
|
||||
swimmingLocationType: .unknown,
|
||||
lapLength: .init(repeating: 42, count: 3),
|
||||
startDate: .now.addingTimeInterval(-100),
|
||||
endDate: .now,
|
||||
duration: 100.0,
|
||||
metadata: .init(repeating: 42, count: 3))
|
||||
)
|
||||
}
|
||||
}
|
@ -1,21 +1,38 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// HealthImport
|
||||
//
|
||||
// Created by iMac on 20.01.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import HealthKit
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite")
|
||||
|
||||
@StateObject var database: HealthDatabase = {
|
||||
try! .init(fileUrl: databaseFileUrl!)
|
||||
}()
|
||||
|
||||
@State var navigationPath: NavigationPath = .init()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
List {
|
||||
ForEach(database.workouts) { workout in
|
||||
NavigationLink(value: workout) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(workout.typeString)
|
||||
Text(workout.dateString)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Workouts")
|
||||
.navigationDestination(for: Workout.self) {
|
||||
WorkoutDetailView(workout: $0)
|
||||
.environmentObject(database)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
45
HealthImport/Database Entries/DBWorkout.swift
Normal file
45
HealthImport/Database Entries/DBWorkout.swift
Normal file
@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct DBWorkout {
|
||||
|
||||
private static let table = Table("workouts")
|
||||
|
||||
private static let rowDataId = Expression<Int>("data_id")
|
||||
|
||||
private static let rowTotalDistance = Expression<Double?>("total_distance")
|
||||
|
||||
private static let rowGoalType = Expression<Int?>("goal_type")
|
||||
|
||||
private static let rowGoal = Expression<Double?>("goal")
|
||||
|
||||
private static let rowCondenserVersion = Expression<Int?>("condenser_version")
|
||||
|
||||
private static let rowCondenserDate = Expression<Double?>("condenser_date")
|
||||
|
||||
static func readAll(in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table).map(Self.init(row:))
|
||||
}
|
||||
|
||||
let dataId: Int
|
||||
|
||||
let totalDistance: Double?
|
||||
|
||||
let goalType: Int?
|
||||
|
||||
let goal: Double?
|
||||
|
||||
let condenserVersion: Int?
|
||||
|
||||
let condenserDate: Double?
|
||||
|
||||
init(row: Row) {
|
||||
self.dataId = row[DBWorkout.rowDataId]
|
||||
self.totalDistance = row[DBWorkout.rowTotalDistance]
|
||||
self.goalType = row[DBWorkout.rowGoalType]
|
||||
self.goal = row[DBWorkout.rowGoal]
|
||||
self.condenserVersion = row[DBWorkout.rowCondenserVersion]
|
||||
self.condenserDate = row[DBWorkout.rowCondenserDate]
|
||||
}
|
||||
}
|
||||
|
79
HealthImport/Database Entries/DBWorkoutActivity.swift
Normal file
79
HealthImport/Database Entries/DBWorkoutActivity.swift
Normal file
@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct DBWorkoutActivity {
|
||||
|
||||
private static let table = Table("workout_activities")
|
||||
|
||||
private static let rowId = Expression<Int>("ROWID")
|
||||
|
||||
private static let rowUUID = Expression<Data>("uuid")
|
||||
|
||||
private static let rowOwnerId = Expression<Int>("owner_id")
|
||||
|
||||
private static let rowIsPrimaryActivity = Expression<Bool>("is_primary_activity")
|
||||
|
||||
private static let rowActivityType = Expression<Int>("activity_type")
|
||||
|
||||
private static let rowLocationType = Expression<Int>("location_type")
|
||||
|
||||
private static let rowSwimmingLocationType = Expression<Int>("swimming_location_type")
|
||||
|
||||
private static let rowLapLength = Expression<Data?>("lap_length")
|
||||
|
||||
private static let rowStartDate = Expression<Double>("start_date")
|
||||
|
||||
private static let rowEndDate = Expression<Double>("end_date")
|
||||
|
||||
private static let rowDuration = Expression<Double>("duration")
|
||||
|
||||
private static let rowMetadata = Expression<Data?>("metadata")
|
||||
|
||||
static func readAll(in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table).map(Self.init)
|
||||
}
|
||||
|
||||
static func activities(for workoutId: Int, in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table.filter(rowOwnerId == workoutId)).map(Self.init)
|
||||
}
|
||||
|
||||
let id: Int
|
||||
|
||||
let uuid: Data
|
||||
|
||||
let ownerId: Int
|
||||
|
||||
let isPrimaryActivity: Bool
|
||||
|
||||
let activityType: Int
|
||||
|
||||
let locationType: Int
|
||||
|
||||
let swimmingLocationType: Int
|
||||
|
||||
let lapLength: Data?
|
||||
|
||||
#warning("Fix timezone for dates")
|
||||
let startDate: Double
|
||||
|
||||
let endDate: Double
|
||||
|
||||
let duration: Double
|
||||
|
||||
let metadata: Data?
|
||||
|
||||
init(row: Row) {
|
||||
self.id = row[DBWorkoutActivity.rowId]
|
||||
self.uuid = row[DBWorkoutActivity.rowUUID]
|
||||
self.ownerId = row[DBWorkoutActivity.rowOwnerId]
|
||||
self.isPrimaryActivity = row[DBWorkoutActivity.rowIsPrimaryActivity]
|
||||
self.activityType = row[DBWorkoutActivity.rowActivityType]
|
||||
self.locationType = row[DBWorkoutActivity.rowLocationType]
|
||||
self.swimmingLocationType = row[DBWorkoutActivity.rowSwimmingLocationType]
|
||||
self.lapLength = row[DBWorkoutActivity.rowLapLength]
|
||||
self.startDate = row[DBWorkoutActivity.rowStartDate]
|
||||
self.endDate = row[DBWorkoutActivity.rowEndDate]
|
||||
self.duration = row[DBWorkoutActivity.rowDuration]
|
||||
self.metadata = row[DBWorkoutActivity.rowMetadata]
|
||||
}
|
||||
}
|
53
HealthImport/Database Entries/DBWorkoutEvent.swift
Normal file
53
HealthImport/Database Entries/DBWorkoutEvent.swift
Normal file
@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
struct DBWorkoutEvent {
|
||||
|
||||
private static let table = Table("workout_events")
|
||||
|
||||
private static let rowOwnerId = Expression<Int>("owner_id")
|
||||
|
||||
private static let rowDate = Expression<Double>("date")
|
||||
|
||||
private static let rowType = Expression<Int>("type")
|
||||
|
||||
private static let rowDuration = Expression<Double>("duration")
|
||||
|
||||
private static let rowMetadata = Expression<Data?>("metadata")
|
||||
|
||||
private static let rowSessionUUID = Expression<Data?>("session_uuid")
|
||||
|
||||
private static let rowError = Expression<Data?>("error")
|
||||
|
||||
static func readAll(in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table).map(Self.init)
|
||||
}
|
||||
|
||||
static func events(for workoutId: Int, in database: Connection) throws -> [Self] {
|
||||
try database.prepare(table.filter(rowOwnerId == workoutId)).map(Self.init)
|
||||
}
|
||||
|
||||
let ownerId: Int
|
||||
|
||||
let date: Double
|
||||
|
||||
let type: Int
|
||||
|
||||
let duration: Double
|
||||
|
||||
let metadata: Data?
|
||||
|
||||
let sessionUUID: Data?
|
||||
|
||||
let error: Data?
|
||||
|
||||
init(row: Row) {
|
||||
self.ownerId = row[DBWorkoutEvent.rowOwnerId]
|
||||
self.date = row[DBWorkoutEvent.rowDate]
|
||||
self.type = row[DBWorkoutEvent.rowType]
|
||||
self.duration = row[DBWorkoutEvent.rowDuration]
|
||||
self.metadata = row[DBWorkoutEvent.rowMetadata]
|
||||
self.sessionUUID = row[DBWorkoutEvent.rowSessionUUID]
|
||||
self.error = row[DBWorkoutEvent.rowError]
|
||||
}
|
||||
}
|
54
HealthImport/DetailRow.swift
Normal file
54
HealthImport/DetailRow.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DetailRow: View {
|
||||
|
||||
let title: String
|
||||
|
||||
let value: String
|
||||
|
||||
init(_ title: String, value: String) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init(_ title: String, value: CustomStringConvertible?) {
|
||||
self.title = title
|
||||
self.value = value?.description ?? "-"
|
||||
}
|
||||
|
||||
init(_ title: String, date value: Date?) {
|
||||
self.title = title
|
||||
self.value = value?.timeAndDateText ?? "-"
|
||||
}
|
||||
|
||||
init(_ title: String, kilometer: Double?) {
|
||||
self.title = title
|
||||
self.value = kilometer?.distanceAsKilometer ?? "-"
|
||||
}
|
||||
|
||||
init(_ title: String, duration: TimeInterval?) {
|
||||
self.title = title
|
||||
self.value = duration?.durationString ?? "-"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
DetailRow("Title", value: "Some")
|
||||
DetailRow("Convertible", value: 123)
|
||||
DetailRow("Optional", value: nil)
|
||||
DetailRow("Date", date: .now)
|
||||
DetailRow("Distance", kilometer: 123.4)
|
||||
DetailRow("Duration", duration: 678.9)
|
||||
}
|
||||
}
|
31
HealthImport/EventDetailView.swift
Normal file
31
HealthImport/EventDetailView.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EventDetailView: View {
|
||||
|
||||
let event: WorkoutEvent
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
DetailRow("Date", date: event.date)
|
||||
DetailRow("Type", value: event.type)
|
||||
DetailRow("Duration", duration: event.duration)
|
||||
DetailRow("Metadata", value: event.metadata)
|
||||
DetailRow("Session UUID", value: event.sessionUUID)
|
||||
DetailRow("Error", value: event.error)
|
||||
}
|
||||
.navigationTitle("Event")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
EventDetailView(event: .init(
|
||||
date: .now,
|
||||
type: .pause,
|
||||
duration: 12.3,
|
||||
metadata: .init(repeating: 42, count: 2),
|
||||
sessionUUID: .init(repeating: 42, count: 3),
|
||||
error: nil)
|
||||
)
|
||||
}
|
||||
}
|
37
HealthImport/HealthDatabase.swift
Normal file
37
HealthImport/HealthDatabase.swift
Normal file
@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
final class HealthDatabase: ObservableObject {
|
||||
|
||||
let fileUrl: URL
|
||||
|
||||
let database: Connection
|
||||
|
||||
@Published
|
||||
var workouts: [Workout] = []
|
||||
|
||||
init(fileUrl: URL) throws {
|
||||
self.fileUrl = fileUrl
|
||||
self.database = try Connection(fileUrl.path)
|
||||
DispatchQueue.global().async {
|
||||
self.readAllWorkouts()
|
||||
}
|
||||
}
|
||||
|
||||
func readAllWorkouts() {
|
||||
do {
|
||||
let dbWorkouts = try DBWorkout.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)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.workouts = workouts
|
||||
}
|
||||
} catch {
|
||||
print("Failed to read workouts: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
93
HealthImport/Model/Workout.swift
Normal file
93
HealthImport/Model/Workout.swift
Normal file
@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
private let df: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.timeZone = .current
|
||||
df.dateStyle = .short
|
||||
df.timeStyle = .short
|
||||
return df
|
||||
}()
|
||||
|
||||
struct Workout {
|
||||
|
||||
let id: Int
|
||||
|
||||
/// The distance in km (?)
|
||||
let totalDistance: Double?
|
||||
|
||||
let goalType: Int?
|
||||
|
||||
let goal: Double?
|
||||
|
||||
let condenserVersion: Int?
|
||||
|
||||
let condenserDate: Date?
|
||||
|
||||
let events: [WorkoutEvent]
|
||||
|
||||
let activities: [WorkoutActivity]
|
||||
|
||||
var firstActivityDate: Date? {
|
||||
activities.map { $0.startDate }.min()
|
||||
}
|
||||
|
||||
var firstEventDate: Date? {
|
||||
events.map { $0.date }.min()
|
||||
}
|
||||
|
||||
var firstAvailableDate: Date? {
|
||||
[condenserDate, firstEventDate, firstActivityDate].compactMap { $0 }.min()
|
||||
}
|
||||
|
||||
var dateString: String {
|
||||
guard let firstAvailableDate else {
|
||||
return "No date"
|
||||
}
|
||||
return df.string(from: firstAvailableDate)
|
||||
}
|
||||
|
||||
var typeString: String {
|
||||
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] = []) {
|
||||
self.id = id
|
||||
self.totalDistance = totalDistance
|
||||
self.goalType = goalType
|
||||
self.goal = goal
|
||||
self.condenserVersion = condenserVersion
|
||||
self.condenserDate = condenserDate
|
||||
self.events = events
|
||||
self.activities = activities
|
||||
}
|
||||
}
|
||||
|
||||
extension Workout {
|
||||
|
||||
init(entry: DBWorkout, events: [DBWorkoutEvent], activities: [DBWorkoutActivity]) {
|
||||
self.id = entry.dataId
|
||||
self.totalDistance = entry.totalDistance
|
||||
self.goalType = entry.goalType
|
||||
self.goal = entry.goal
|
||||
self.condenserVersion = entry.condenserVersion
|
||||
self.condenserDate = entry.condenserDate.map { Date(timeIntervalSinceReferenceDate: $0) }
|
||||
self.events = events.map(WorkoutEvent.init)
|
||||
self.activities = activities.map(WorkoutActivity.init)
|
||||
}
|
||||
}
|
||||
|
||||
extension Workout: Identifiable { }
|
||||
|
||||
extension Workout: Equatable {
|
||||
static func == (lhs: Workout, rhs: Workout) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Workout: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
62
HealthImport/Model/WorkoutActivity.swift
Normal file
62
HealthImport/Model/WorkoutActivity.swift
Normal file
@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
struct WorkoutActivity {
|
||||
|
||||
let uuid: Data
|
||||
|
||||
let isPrimaryActivity: Bool
|
||||
|
||||
let activityType: HKWorkoutActivityType
|
||||
|
||||
let locationType: HKWorkoutSessionLocationType
|
||||
|
||||
let swimmingLocationType: HKWorkoutSwimmingLocationType
|
||||
|
||||
let lapLength: Data?
|
||||
|
||||
let startDate: Date
|
||||
|
||||
let endDate: Date
|
||||
|
||||
let duration: TimeInterval
|
||||
|
||||
let metadata: Data?
|
||||
}
|
||||
|
||||
extension WorkoutActivity {
|
||||
|
||||
init(entry: DBWorkoutActivity) {
|
||||
self.uuid = entry.uuid
|
||||
self.isPrimaryActivity = entry.isPrimaryActivity
|
||||
self.activityType = .init(rawValue: UInt(entry.activityType))!
|
||||
self.locationType = .init(rawValue: entry.locationType)!
|
||||
self.swimmingLocationType = .init(rawValue: entry.swimmingLocationType)!
|
||||
self.lapLength = entry.lapLength
|
||||
self.startDate = Date(timeIntervalSinceReferenceDate: entry.startDate)
|
||||
self.endDate = Date(timeIntervalSinceReferenceDate: entry.endDate)
|
||||
self.duration = entry.duration
|
||||
self.metadata = entry.metadata
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutActivity: Equatable {
|
||||
|
||||
static func == (lhs: WorkoutActivity, rhs: WorkoutActivity) -> Bool {
|
||||
lhs.uuid == rhs.uuid
|
||||
}
|
||||
}
|
||||
|
||||
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(uuid)
|
||||
}
|
||||
}
|
51
HealthImport/Model/WorkoutEvent.swift
Normal file
51
HealthImport/Model/WorkoutEvent.swift
Normal file
@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
struct WorkoutEvent {
|
||||
|
||||
let date: Date
|
||||
|
||||
let type: HKWorkoutEventType
|
||||
|
||||
let duration: TimeInterval
|
||||
|
||||
let metadata: Data?
|
||||
|
||||
let sessionUUID: Data?
|
||||
|
||||
let error: Data?
|
||||
|
||||
}
|
||||
|
||||
extension WorkoutEvent {
|
||||
|
||||
init(entry: DBWorkoutEvent) {
|
||||
self.date = Date(timeIntervalSinceReferenceDate: entry.date)
|
||||
self.type = .init(rawValue: entry.type)!
|
||||
self.duration = entry.duration
|
||||
self.metadata = entry.metadata
|
||||
self.sessionUUID = entry.sessionUUID
|
||||
self.error = entry.error
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutEvent: Equatable {
|
||||
|
||||
static func == (lhs: WorkoutEvent, rhs: WorkoutEvent) -> Bool {
|
||||
lhs.date == rhs.date
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
63
HealthImport/Support/Date+Extensions.swift
Normal file
63
HealthImport/Support/Date+Extensions.swift
Normal file
@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateStyle = .short
|
||||
df.timeStyle = .short
|
||||
return df
|
||||
}()
|
||||
|
||||
private let justDateFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateStyle = .short
|
||||
df.timeStyle = .none
|
||||
return df
|
||||
}()
|
||||
|
||||
private let timeFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateStyle = .none
|
||||
df.timeStyle = .short
|
||||
return df
|
||||
}()
|
||||
|
||||
extension Date {
|
||||
|
||||
var durationSinceNowText: String {
|
||||
let secondsAgo = -timeIntervalSinceNow
|
||||
guard secondsAgo > 2.5 else {
|
||||
return "Now"
|
||||
}
|
||||
guard secondsAgo > 60 else {
|
||||
let multiples = Int((secondsAgo / 5).rounded()) * 5
|
||||
return "\(multiples) seconds ago"
|
||||
}
|
||||
let minutesAgo = Int((secondsAgo / 60).rounded())
|
||||
guard minutesAgo > 1 else {
|
||||
return "\(minutesAgo) minute ago"
|
||||
}
|
||||
guard minutesAgo > 60 else {
|
||||
return "\(minutesAgo) minutes ago"
|
||||
}
|
||||
let hoursAgo = Int(Double(minutesAgo / 60).rounded())
|
||||
guard hoursAgo > 1 else {
|
||||
return "\(hoursAgo) hour ago"
|
||||
}
|
||||
guard hoursAgo > 24 else {
|
||||
return "\(hoursAgo) hours ago"
|
||||
}
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
var timeOrDateText: String {
|
||||
if Calendar.current.isDateInToday(self) {
|
||||
return timeFormatter.string(from: self)
|
||||
}
|
||||
return justDateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
var timeAndDateText: String {
|
||||
dateFormatter.string(from: self)
|
||||
}
|
||||
}
|
187
HealthImport/Support/HKWorkoutActivityType+Extensions.swift
Normal file
187
HealthImport/Support/HKWorkoutActivityType+Extensions.swift
Normal file
@ -0,0 +1,187 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
extension HKWorkoutActivityType: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .climbing:
|
||||
return "Climbing"
|
||||
case .cycling:
|
||||
return "Cycling"
|
||||
case .hiking:
|
||||
return "Hiking"
|
||||
case .hockey:
|
||||
return "Hockey"
|
||||
case .other:
|
||||
return "Other"
|
||||
case .rowing:
|
||||
return "Rowing"
|
||||
case .running:
|
||||
return "Running"
|
||||
case .swimming:
|
||||
return "Swimming"
|
||||
case .yoga:
|
||||
return "Yoga"
|
||||
case .walking:
|
||||
return "Walking"
|
||||
case .americanFootball:
|
||||
return "American Football"
|
||||
case .archery:
|
||||
return "Archery"
|
||||
case .australianFootball:
|
||||
return "Australian Football"
|
||||
case .badminton:
|
||||
return "Badminton"
|
||||
case .baseball:
|
||||
return "Baseball"
|
||||
case .basketball:
|
||||
return "Basketball"
|
||||
case .bowling:
|
||||
return "Bowling"
|
||||
case .boxing:
|
||||
return "Boxing"
|
||||
case .cricket:
|
||||
return "Cricket"
|
||||
case .crossTraining:
|
||||
return "Cross Training"
|
||||
case .curling:
|
||||
return "Curling"
|
||||
case .dance:
|
||||
return "Dance"
|
||||
case .danceInspiredTraining:
|
||||
return "Dance Inspired Training"
|
||||
case .elliptical:
|
||||
return "Elliptical"
|
||||
case .equestrianSports:
|
||||
return "Equestrian Sports"
|
||||
case .fencing:
|
||||
return "Fencing"
|
||||
case .fishing:
|
||||
return "Fishing"
|
||||
case .functionalStrengthTraining:
|
||||
return "Functional Strength Training"
|
||||
case .golf:
|
||||
return "Golf"
|
||||
case .gymnastics:
|
||||
return "Gymnastics"
|
||||
case .handball:
|
||||
return "Handball"
|
||||
case .hunting:
|
||||
return "Hunting"
|
||||
case .lacrosse:
|
||||
return "Lacrosse"
|
||||
case .martialArts:
|
||||
return "Martial Arts"
|
||||
case .mindAndBody:
|
||||
return "Mind And Body"
|
||||
case .mixedMetabolicCardioTraining:
|
||||
return "Mixed Metabolic Cardio Training"
|
||||
case .paddleSports:
|
||||
return "Paddle Sports"
|
||||
case .play:
|
||||
return "Play"
|
||||
case .preparationAndRecovery:
|
||||
return "Preparation and Recovery"
|
||||
case .racquetball:
|
||||
return "Racquetball"
|
||||
case .rugby:
|
||||
return "Rugby"
|
||||
case .sailing:
|
||||
return "Sailing"
|
||||
case .skatingSports:
|
||||
return "Skating Sports"
|
||||
case .snowSports:
|
||||
return "Snow Sports"
|
||||
case .soccer:
|
||||
return "Soccer"
|
||||
case .softball:
|
||||
return "Softball"
|
||||
case .squash:
|
||||
return "Squash"
|
||||
case .stairClimbing:
|
||||
return "Stair Climbing"
|
||||
case .surfingSports:
|
||||
return "Surfing Sports"
|
||||
case .tableTennis:
|
||||
return "Table Tennis"
|
||||
case .tennis:
|
||||
return "Tennis"
|
||||
case .trackAndField:
|
||||
return "Track And Field"
|
||||
case .traditionalStrengthTraining:
|
||||
return "Traditional Strength Training"
|
||||
case .volleyball:
|
||||
return "Volleyball"
|
||||
case .waterFitness:
|
||||
return "Water Fitness"
|
||||
case .waterPolo:
|
||||
return "Water Polo"
|
||||
case .waterSports:
|
||||
return "Water Sports"
|
||||
case .wrestling:
|
||||
return "Wrestling"
|
||||
case .barre:
|
||||
return "Barre"
|
||||
case .coreTraining:
|
||||
return "Core Training"
|
||||
case .crossCountrySkiing:
|
||||
return "Cross Country Skiing"
|
||||
case .downhillSkiing:
|
||||
return "Downholl Skiing"
|
||||
case .flexibility:
|
||||
return "Flexibility"
|
||||
case .highIntensityIntervalTraining:
|
||||
return "High Intensity Interval Training"
|
||||
case .jumpRope:
|
||||
return "Jump Rope"
|
||||
case .kickboxing:
|
||||
return "Kickboxing"
|
||||
case .pilates:
|
||||
return "Pilates"
|
||||
case .snowboarding:
|
||||
return "Snowboarding"
|
||||
case .stairs:
|
||||
return "Stairs"
|
||||
case .stepTraining:
|
||||
return "Step Training"
|
||||
case .wheelchairWalkPace:
|
||||
return "Wheelchair Walk Pace"
|
||||
case .wheelchairRunPace:
|
||||
return "Wheelchair Run Pace"
|
||||
case .taiChi:
|
||||
return "Tai Chi"
|
||||
case .mixedCardio:
|
||||
return "Mixed Cardio"
|
||||
case .handCycling:
|
||||
return "Hand Cycling"
|
||||
case .discSports:
|
||||
return "Disc Sports"
|
||||
case .fitnessGaming:
|
||||
return "Fitness Gaming"
|
||||
case .cardioDance:
|
||||
return "Cardio Dance"
|
||||
case .socialDance:
|
||||
return "Social Dance"
|
||||
case .pickleball:
|
||||
return "Pickleball"
|
||||
case .cooldown:
|
||||
return "Cooldown"
|
||||
case .swimBikeRun:
|
||||
return "Triathlon"
|
||||
case .transition:
|
||||
return "Transition"
|
||||
case .underwaterDiving:
|
||||
return "Underwater Diving"
|
||||
@unknown default:
|
||||
return "\(rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HKWorkoutActivityType: Comparable {
|
||||
|
||||
public static func < (lhs: HKWorkoutActivityType, rhs: HKWorkoutActivityType) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
20
HealthImport/Support/HKWorkoutEventType+Extensions.swift
Normal file
20
HealthImport/Support/HKWorkoutEventType+Extensions.swift
Normal file
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
extension HKWorkoutEventType: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .pause: return "Pause"
|
||||
case .resume: return "Resume"
|
||||
case .lap: return "Lap"
|
||||
case .marker: return "Marker"
|
||||
case .motionPaused: return "Motion Paused"
|
||||
case .motionResumed: return "Motino Resumed"
|
||||
case .segment: return "Segment"
|
||||
case .pauseOrResumeRequest: return "Pause or Resume Request"
|
||||
@unknown default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
extension HKWorkoutSessionLocationType: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return "Unknown"
|
||||
case .indoor:
|
||||
return "Indoor"
|
||||
case .outdoor:
|
||||
return "Outdoor"
|
||||
@unknown default:
|
||||
return "Unknown default"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
import HealthKit
|
||||
|
||||
extension HKWorkoutSwimmingLocationType: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return "Unknown"
|
||||
case .pool:
|
||||
return "Pool"
|
||||
case .openWater:
|
||||
return "Open Water"
|
||||
@unknown default:
|
||||
return "Unknown default"
|
||||
}
|
||||
}
|
||||
}
|
11
HealthImport/Support/Optional+Extensions.swift
Normal file
11
HealthImport/Support/Optional+Extensions.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
extension Optional {
|
||||
|
||||
func map<T>(_ transform: (Wrapped) -> T) -> T? {
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return transform(self)
|
||||
}
|
||||
}
|
33
HealthImport/Support/TimeInterval+Extensions.swift
Normal file
33
HealthImport/Support/TimeInterval+Extensions.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
|
||||
var roundedInt: Int {
|
||||
Int(rounded())
|
||||
}
|
||||
|
||||
var distanceAsKilometer: String {
|
||||
guard self < 0.1 else {
|
||||
return String(format: "%.2f km", self)
|
||||
}
|
||||
return String(format: "%.1f m", self / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
|
||||
var durationString: String {
|
||||
let totalSeconds = roundedInt
|
||||
guard totalSeconds >= 60 else {
|
||||
return String(format: "%.3f s", self)
|
||||
}
|
||||
let seconds = totalSeconds % 60
|
||||
let totalMinutes = totalSeconds / 60
|
||||
guard totalMinutes >= 60 else {
|
||||
return String(format: "%d:%02d", totalMinutes, seconds)
|
||||
}
|
||||
let minutes = totalMinutes % 60
|
||||
let totalHours = totalMinutes / 60
|
||||
return String(format: "%d:%02d:%02d", totalHours, minutes, seconds)
|
||||
}
|
||||
}
|
83
HealthImport/WorkoutDetailView.swift
Normal file
83
HealthImport/WorkoutDetailView.swift
Normal file
@ -0,0 +1,83 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WorkoutDetailView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var database: HealthDatabase
|
||||
|
||||
let workout: Workout
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Info") {
|
||||
DetailRow("ID", value: workout.id)
|
||||
DetailRow("Total Distance", kilometer: workout.totalDistance)
|
||||
DetailRow("Goal Type", value: workout.goalType)
|
||||
DetailRow("Goal", value: workout.goal)
|
||||
DetailRow("Condenser Version", value: workout.condenserVersion)
|
||||
DetailRow("Condenser Date", date: workout.condenserDate)
|
||||
}
|
||||
if !workout.events.isEmpty {
|
||||
Section("Events") {
|
||||
ForEach(workout.events, id: \.date) { event in
|
||||
NavigationLink(value: event) {
|
||||
DetailRow(event.type.description, date: event.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !workout.activities.isEmpty {
|
||||
Section("Activities") {
|
||||
ForEach(workout.activities, id: \.startDate) { activity in
|
||||
NavigationLink(value: activity) {
|
||||
DetailRow(activity.activityType.description,
|
||||
date: activity.startDate)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(workout.typeString)
|
||||
.navigationDestination(for: WorkoutActivity.self) { activity in
|
||||
ActivityDetailView(activity: activity)
|
||||
}
|
||||
.navigationDestination(for: WorkoutEvent.self) { event in
|
||||
EventDetailView(event: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
WorkoutDetailView(workout: .init(
|
||||
id: 123,
|
||||
totalDistance: 234.5,
|
||||
goalType: 3,
|
||||
goal: 345.6,
|
||||
condenserVersion: 1,
|
||||
condenserDate: .now,
|
||||
events: [
|
||||
.init(
|
||||
date: .now,
|
||||
type: .pause,
|
||||
duration: 12.3,
|
||||
metadata: .init(repeating: 42, count: 2),
|
||||
sessionUUID: .init(repeating: 42, count: 3),
|
||||
error: nil)
|
||||
],
|
||||
activities: [
|
||||
.init(
|
||||
uuid: .init(repeating: 42, count: 3),
|
||||
isPrimaryActivity: true,
|
||||
activityType: .running,
|
||||
locationType: .outdoor,
|
||||
swimmingLocationType: .unknown,
|
||||
lapLength: .init(repeating: 42, count: 3),
|
||||
startDate: .now.addingTimeInterval(-100),
|
||||
endDate: .now,
|
||||
duration: 100.0,
|
||||
metadata: .init(repeating: 42, count: 3))
|
||||
]))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user