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 */; };
|
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850025C2B5C273C00E7D4DB /* ContentView.swift */; };
|
||||||
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; };
|
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8850025E2B5C273E00E7D4DB /* Assets.xcassets */; };
|
||||||
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 885002612B5C273E00E7D4DB /* Preview 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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -19,6 +39,25 @@
|
|||||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -26,6 +65,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -51,10 +91,19 @@
|
|||||||
885002592B5C273C00E7D4DB /* HealthImport */ = {
|
885002592B5C273C00E7D4DB /* HealthImport */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
8850026A2B5C276B00E7D4DB /* Resources */,
|
||||||
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
||||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
||||||
|
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
||||||
|
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
||||||
|
8850029C2B5D197300E7D4DB /* EventDetailView.swift */,
|
||||||
|
885002942B5D147100E7D4DB /* DetailRow.swift */,
|
||||||
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
|
8850025E2B5C273E00E7D4DB /* Assets.xcassets */,
|
||||||
885002602B5C273E00E7D4DB /* Preview Content */,
|
885002602B5C273E00E7D4DB /* Preview Content */,
|
||||||
|
885002702B5C299900E7D4DB /* HealthDatabase.swift */,
|
||||||
|
885002802B5C37A800E7D4DB /* Database Entries */,
|
||||||
|
885002812B5C37B700E7D4DB /* Model */,
|
||||||
|
885002832B5C37C600E7D4DB /* Support */,
|
||||||
);
|
);
|
||||||
path = HealthImport;
|
path = HealthImport;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -67,6 +116,48 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
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 */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -83,6 +174,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = HealthImport;
|
name = HealthImport;
|
||||||
|
packageProductDependencies = (
|
||||||
|
885002762B5C2FC400E7D4DB /* SQLite */,
|
||||||
|
);
|
||||||
productName = HealthImport;
|
productName = HealthImport;
|
||||||
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -111,6 +205,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 8850024E2B5C273C00E7D4DB;
|
mainGroup = 8850024E2B5C273C00E7D4DB;
|
||||||
|
packageReferences = (
|
||||||
|
885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||||
|
);
|
||||||
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -125,6 +222,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
8850026C2B5C278600E7D4DB /* healthdb_secure.sqlite in Resources */,
|
||||||
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */,
|
885002622B5C273E00E7D4DB /* Preview Assets.xcassets in Resources */,
|
||||||
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */,
|
8850025F2B5C273E00E7D4DB /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
@ -137,7 +235,25 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
885002872B5C7FA900E7D4DB /* HKWorkoutActivityType+Extensions.swift in Sources */,
|
||||||
8850025D2B5C273C00E7D4DB /* ContentView.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 */,
|
8850025B2B5C273C00E7D4DB /* HealthImportApp.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -344,6 +460,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* 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 */;
|
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>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</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>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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 SwiftUI
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
struct ContentView: View {
|
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 {
|
var body: some View {
|
||||||
VStack {
|
NavigationStack(path: $navigationPath) {
|
||||||
Image(systemName: "globe")
|
VStack {
|
||||||
.imageScale(.large)
|
List {
|
||||||
.foregroundStyle(.tint)
|
ForEach(database.workouts) { workout in
|
||||||
Text("Hello, world!")
|
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