Search samples, view metadata, events
This commit is contained in:
parent
d99d83a085
commit
d1a7e2b441
@ -39,6 +39,10 @@
|
|||||||
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; };
|
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; };
|
||||||
E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA02B99FFDD00BAD02E /* HKDatabase */; };
|
E2A38EA12B99FFDD00BAD02E /* HKDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA02B99FFDD00BAD02E /* HKDatabase */; };
|
||||||
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; };
|
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */; };
|
||||||
|
E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */; };
|
||||||
|
E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */; };
|
||||||
|
E2A38EAA2B9C862600BAD02E /* WorkoutEventsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EA92B9C862600BAD02E /* WorkoutEventsView.swift */; };
|
||||||
|
E2A38EAC2B9C8E4B00BAD02E /* WorkoutMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A38EAB2B9C8E4B00BAD02E /* WorkoutMetadataView.swift */; };
|
||||||
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
|
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; };
|
||||||
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
|
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -71,6 +75,9 @@
|
|||||||
E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = "<group>"; };
|
E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutEvent+Mock.swift"; sourceTree = "<group>"; };
|
||||||
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = "<group>"; };
|
E2A38EA22B9A024500BAD02E /* Workout+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workout+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHealthStoreView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A38EA92B9C862600BAD02E /* WorkoutEventsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEventsView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A38EAB2B9C8E4B00BAD02E /* WorkoutMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutMetadataView.swift; sourceTree = "<group>"; };
|
||||||
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = "<group>"; };
|
E2FDFF342B6E59030080A7B3 /* HealthImport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthImport.entitlements; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@ -86,6 +93,7 @@
|
|||||||
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
|
885002772B5C2FC400E7D4DB /* SQLite in Frameworks */,
|
||||||
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */,
|
885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */,
|
||||||
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */,
|
885002A82B5D296700E7D4DB /* DequeModule in Frameworks */,
|
||||||
|
E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */,
|
||||||
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */,
|
E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -116,7 +124,10 @@
|
|||||||
8850026A2B5C276B00E7D4DB /* Resources */,
|
8850026A2B5C276B00E7D4DB /* Resources */,
|
||||||
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
8850025A2B5C273C00E7D4DB /* HealthImportApp.swift */,
|
||||||
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
8850025C2B5C273C00E7D4DB /* ContentView.swift */,
|
||||||
|
E2A38EA42B9C6EA900BAD02E /* SearchHealthStoreView.swift */,
|
||||||
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
8850028C2B5D0B5000E7D4DB /* WorkoutDetailView.swift */,
|
||||||
|
E2A38EAB2B9C8E4B00BAD02E /* WorkoutMetadataView.swift */,
|
||||||
|
E2A38EA92B9C862600BAD02E /* WorkoutEventsView.swift */,
|
||||||
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
885002922B5D129300E7D4DB /* ActivityDetailView.swift */,
|
||||||
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */,
|
E27BC68B2B5FC842003A8873 /* ActivitySamplesView.swift */,
|
||||||
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */,
|
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */,
|
||||||
@ -192,6 +203,7 @@
|
|||||||
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */,
|
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */,
|
||||||
E20881D22B76912000D41D95 /* HealthKitExtensions */,
|
E20881D22B76912000D41D95 /* HealthKitExtensions */,
|
||||||
E2A38EA02B99FFDD00BAD02E /* HKDatabase */,
|
E2A38EA02B99FFDD00BAD02E /* HKDatabase */,
|
||||||
|
E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */,
|
||||||
);
|
);
|
||||||
productName = HealthImport;
|
productName = HealthImport;
|
||||||
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */;
|
||||||
@ -227,6 +239,7 @@
|
|||||||
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
|
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
|
||||||
E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */,
|
E20881D12B76912000D41D95 /* XCRemoteSwiftPackageReference "HealthKitExtensions" */,
|
||||||
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */,
|
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */,
|
||||||
|
E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
productRefGroup = 885002582B5C273C00E7D4DB /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -257,6 +270,7 @@
|
|||||||
files = (
|
files = (
|
||||||
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */,
|
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */,
|
||||||
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */,
|
E2A38EA32B9A024500BAD02E /* Workout+Extensions.swift in Sources */,
|
||||||
|
E2A38EAC2B9C8E4B00BAD02E /* WorkoutMetadataView.swift in Sources */,
|
||||||
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
|
E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */,
|
||||||
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
|
8850025D2B5C273C00E7D4DB /* ContentView.swift in Sources */,
|
||||||
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */,
|
8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */,
|
||||||
@ -270,10 +284,12 @@
|
|||||||
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
|
E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */,
|
||||||
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
|
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */,
|
||||||
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */,
|
||||||
|
E2A38EAA2B9C862600BAD02E /* WorkoutEventsView.swift in Sources */,
|
||||||
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
|
||||||
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
|
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
|
||||||
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
|
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
|
||||||
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
|
||||||
|
E2A38EA52B9C6EA900BAD02E /* SearchHealthStoreView.swift in Sources */,
|
||||||
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
|
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
|
||||||
E20881D52B76944A00D41D95 /* Test.swift in Sources */,
|
E20881D52B76944A00D41D95 /* Test.swift in Sources */,
|
||||||
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
|
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,
|
||||||
@ -512,8 +528,8 @@
|
|||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/christophhagen/HealthKitExtensions";
|
repositoryURL = "https://github.com/christophhagen/HealthKitExtensions";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
branch = main;
|
||||||
minimumVersion = 0.3.0;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */ = {
|
E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */ = {
|
||||||
@ -524,6 +540,14 @@
|
|||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.2.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
|
E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-protobuf.git";
|
repositoryURL = "https://github.com/apple/swift-protobuf.git";
|
||||||
@ -565,6 +589,11 @@
|
|||||||
package = E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */;
|
package = E2A38E9F2B99FFDD00BAD02E /* XCRemoteSwiftPackageReference "iOSHealthDBInterface" */;
|
||||||
productName = HKDatabase;
|
productName = HKDatabase;
|
||||||
};
|
};
|
||||||
|
E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = {
|
E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */;
|
package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "d0067804897f78a2b51cdf96069649aae9f635cdf94333101762cb0c84fd39ae",
|
"originHash" : "b4e05748d8500bbff1c8ae286dbcad777cbcbcfd5780e4d633cf669d8ce257fb",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "healthkitextensions",
|
"identity" : "healthkitextensions",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/christophhagen/HealthKitExtensions",
|
"location" : "https://github.com/christophhagen/HealthKitExtensions",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "88625ad3480ceea974e47f61e1abb05a84896c73",
|
"branch" : "main",
|
||||||
"version" : "0.3.0"
|
"revision" : "02ce75960a2b3fd1d2b7d2c620f519342956690c"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -16,7 +16,16 @@
|
|||||||
"location" : "https://github.com/christophhagen/iOSHealthDBInterface",
|
"location" : "https://github.com/christophhagen/iOSHealthDBInterface",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "d638c367f3bbdbffefdf0a4dde8fc0afd73bbaad"
|
"revision" : "b5acf75f1d5a166cc7a92ebf040160e6471d8ff1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "afd0a1da4ed62bab1413caa6dd6b60a7a7089ed2",
|
||||||
|
"version" : "5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import HealthKit
|
import HealthKit
|
||||||
import HKDatabase
|
import HKDatabase
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@ -30,20 +31,35 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button(action: addWorkouts) {
|
NavigationLink {
|
||||||
Text("Add")
|
SearchHealthStoreView()
|
||||||
|
} label: {
|
||||||
|
Image(systemSymbol: .magnifyingglass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear(perform: getPermissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addWorkouts() {
|
private func getPermissions() {
|
||||||
Task {
|
Task {
|
||||||
|
let store = HKHealthStore()
|
||||||
do {
|
do {
|
||||||
try await insertExamplesOfAllTypes()
|
let success = try await requestAllPermissions(in: store)
|
||||||
|
print("Has permissions: \(success)")
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed: \(error)")
|
print("Error getting permissions: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let workouts = try HealthDatabase.shared.readAllWorkouts()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.workouts = workouts.reversed()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error getting workouts: \(error)")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import HKDatabase
|
import HKDatabase
|
||||||
|
|
||||||
#warning("Load workouts from database and samples from HealthKit")
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct HealthImportApp: App {
|
struct HealthImportApp: App {
|
||||||
|
|
||||||
@ -23,6 +21,21 @@ private extension HealthDatabase {
|
|||||||
static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite")
|
static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite")
|
||||||
|
|
||||||
convenience init() {
|
convenience init() {
|
||||||
try! self.init(fileUrl: HealthDatabase.databaseFileUrl!)
|
let bundleUrl = HealthDatabase.databaseFileUrl!
|
||||||
|
let local = FileManager.default.documentDirectory.appendingPathComponent("db.sqlite")
|
||||||
|
if !FileManager.default.fileExists(atPath: local.path) {
|
||||||
|
try! FileManager.default.copyItem(at: bundleUrl, to: local)
|
||||||
|
}
|
||||||
|
try! self.init(fileUrl: local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
|
||||||
|
var documentDirectory: URL {
|
||||||
|
try! url(
|
||||||
|
for: .documentDirectory,
|
||||||
|
in: .userDomainMask,
|
||||||
|
appropriateFor: nil, create: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
91
HealthImport/SearchHealthStoreView.swift
Normal file
91
HealthImport/SearchHealthStoreView.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import HealthKit
|
||||||
|
import HealthKitExtensions
|
||||||
|
|
||||||
|
struct SearchHealthStoreView: View {
|
||||||
|
|
||||||
|
@State
|
||||||
|
var start: Date = .now.addingTimeInterval(-86400)
|
||||||
|
|
||||||
|
@State
|
||||||
|
var end: Date = .now
|
||||||
|
|
||||||
|
@State
|
||||||
|
var quantity: HKQuantityTypeIdentifier = .heartRate
|
||||||
|
|
||||||
|
@State
|
||||||
|
var isRunningQuery = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var samples: [HKQuantitySample] = []
|
||||||
|
|
||||||
|
private let store = HKHealthStore()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
DatePicker("Start date", selection: $start)
|
||||||
|
DatePicker("End date", selection: $end)
|
||||||
|
Picker("Quantity", selection: $quantity) {
|
||||||
|
ForEach(HKQuantityTypeIdentifier.allCases) { id in
|
||||||
|
Text(id.description).tag(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Button(action: performQuery) {
|
||||||
|
Text("Search")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.disabled(isRunningQuery)
|
||||||
|
Text("\(samples.count) samples found")
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.navigationTitle("Search")
|
||||||
|
}
|
||||||
|
|
||||||
|
func performQuery() {
|
||||||
|
isRunningQuery = true
|
||||||
|
Task {
|
||||||
|
let samples = try await queryHealthStore()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.samples = samples
|
||||||
|
self.isRunningQuery = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryHealthStore() async throws -> [HKQuantitySample] {
|
||||||
|
let sortByDate = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)
|
||||||
|
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
||||||
|
|
||||||
|
let samples = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[HKQuantitySample], Error>) in
|
||||||
|
let query = HKSampleQuery(
|
||||||
|
sampleType: HKQuantityType(quantity),
|
||||||
|
predicate: predicate,
|
||||||
|
limit: HKObjectQueryNoLimit,
|
||||||
|
sortDescriptors: [sortByDate]) { query, samples, error in
|
||||||
|
if let error = error {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let samples = samples else {
|
||||||
|
continuation.resume(returning: [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continuation.resume(returning: samples as! [HKQuantitySample])
|
||||||
|
}
|
||||||
|
store.execute(query)
|
||||||
|
}
|
||||||
|
return samples
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SearchHealthStoreView()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HKQuantityTypeIdentifier: Identifiable {
|
||||||
|
|
||||||
|
public var id: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,25 @@ import SwiftUI
|
|||||||
import Collections
|
import Collections
|
||||||
import HealthKit
|
import HealthKit
|
||||||
import HKDatabase
|
import HKDatabase
|
||||||
|
import HealthKitExtensions
|
||||||
|
import CoreLocation
|
||||||
|
|
||||||
struct WorkoutDetailView: View {
|
struct WorkoutDetailView: View {
|
||||||
|
|
||||||
let workout: Workout
|
let workout: Workout
|
||||||
|
|
||||||
var metadata: [(key: String, value: Any)] {
|
private let store = HKHealthStore()
|
||||||
workout.metadata.sorted { $0.key }
|
|
||||||
}
|
@State
|
||||||
|
var heartRateSamples: [HeartRate] = []
|
||||||
|
|
||||||
|
@State
|
||||||
|
var heartRateSamplesInDatabase: [HeartRate] = []
|
||||||
|
|
||||||
|
@State
|
||||||
|
var locationSamples: [CLLocation] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
@ -31,28 +42,96 @@ struct WorkoutDetailView: View {
|
|||||||
}
|
}
|
||||||
if !workout.events.isEmpty {
|
if !workout.events.isEmpty {
|
||||||
Section("Events") {
|
Section("Events") {
|
||||||
ForEach(workout.events) { event in
|
NavigationLink(value: workout.events) {
|
||||||
NavigationLink(value: event) {
|
DetailRow("Events", value: workout.events.count)
|
||||||
DetailRow(event.type.description, date: event.dateInterval.start)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !workout.metadata.isEmpty {
|
if !workout.metadata.isEmpty {
|
||||||
Section("Metadata") {
|
Section("Metadata") {
|
||||||
ForEach(metadata, id:\.key) { (key, value) in
|
NavigationLink {
|
||||||
DetailRow("\(key)", value: "\(value)")
|
WorkoutMetadataView(metadata: workout.metadata)
|
||||||
|
} label: {
|
||||||
|
DetailRow("Metadata", value: workout.metadata.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Heart Rate") {
|
||||||
|
DetailRow("Count", value: "\(heartRateSamples.count)")
|
||||||
|
DetailRow("Range", value: "\(heartRateSamples.minimumHeartRate) - \(heartRateSamples.maximumHeartRate)")
|
||||||
|
DetailRow("Database count", value: "\(heartRateSamplesInDatabase.count)")
|
||||||
|
DetailRow("Database range", value: "\(heartRateSamplesInDatabase.minimumHeartRate) - \(heartRateSamplesInDatabase.maximumHeartRate)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !locationSamples.isEmpty {
|
||||||
|
Section("Locations") {
|
||||||
|
DetailRow("Count", value: "\(locationSamples.count)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(workout.typeString)
|
.navigationTitle(workout.typeString)
|
||||||
.navigationDestination(for: HKWorkoutActivity.self) { activity in
|
.navigationDestination(for: HKWorkoutActivity.self) { activity in
|
||||||
ActivityDetailView(activity: activity)
|
ActivityDetailView(activity: activity)
|
||||||
}
|
}
|
||||||
.navigationDestination(for: HKWorkoutEvent.self) { event in
|
.navigationDestination(for: [HKWorkoutEvent].self) {
|
||||||
EventDetailView(event: event)
|
WorkoutEventsView(events: $0)
|
||||||
}
|
}
|
||||||
|
.onAppear(perform: loadSamples)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadSamples() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let samples = try await self.loadHeartRateData()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.heartRateSamples = samples
|
||||||
|
}
|
||||||
|
print("Loaded \(samples.count) heart rate samples")
|
||||||
|
} catch {
|
||||||
|
print("Failed to load heart rate samples: \(error)")
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let samples = try self.queryDatabase()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.heartRateSamplesInDatabase = samples
|
||||||
|
}
|
||||||
|
print("Loaded \(samples.count) heart rate samples from database")
|
||||||
|
} catch {
|
||||||
|
print("Failed to load heart rate samples from database: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadHeartRateData() async throws -> [HeartRate] {
|
||||||
|
let sort = SortDescriptor<HKQuantitySample>.init(\.endDate, order: .forward)
|
||||||
|
|
||||||
|
guard let start = workout.firstActivityDate,
|
||||||
|
let end = workout.activities.compactMap({ $0.endDate }).max() else {
|
||||||
|
print("No dates to get heart rates")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Heart rates from \(start) to \(end)")
|
||||||
|
let predicate = HKQuery.predicateForSamples(
|
||||||
|
withStart: start,
|
||||||
|
end: end,
|
||||||
|
options: [])
|
||||||
|
|
||||||
|
return try await store.read(
|
||||||
|
predicate: predicate,
|
||||||
|
sortDescriptors: [sort],
|
||||||
|
limit: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDatabase() throws -> [HeartRate] {
|
||||||
|
guard let start = workout.firstActivityDate,
|
||||||
|
let end = workout.activities.compactMap({ $0.endDate }).max() else {
|
||||||
|
print("No dates to get heart rates")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return try HealthDatabase.shared.samples(from: start, to: end)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,3 +146,25 @@ extension String: Identifiable {
|
|||||||
|
|
||||||
public var id: Self { self }
|
public var id: Self { self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension Array where Element == HeartRate {
|
||||||
|
|
||||||
|
var minimumHeartRate: Int {
|
||||||
|
self.min {
|
||||||
|
$0.value < $1.value
|
||||||
|
}?.beatsPerMinute ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var maximumHeartRate: Int {
|
||||||
|
self.max {
|
||||||
|
$0.value < $1.value
|
||||||
|
}?.beatsPerMinute ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HeartRate {
|
||||||
|
|
||||||
|
var beatsPerMinute: Int {
|
||||||
|
quantity.doubleValue(for: .count().unitDivided(by: .minute())).roundedInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
26
HealthImport/WorkoutEventsView.swift
Normal file
26
HealthImport/WorkoutEventsView.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import HKDatabase
|
||||||
|
import HealthKit
|
||||||
|
|
||||||
|
struct WorkoutEventsView: View {
|
||||||
|
|
||||||
|
let events: [HKWorkoutEvent]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(events) { event in
|
||||||
|
NavigationLink(value: event) {
|
||||||
|
DetailRow(event.type.description, date: event.dateInterval.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Events")
|
||||||
|
.navigationDestination(for: HKWorkoutEvent.self) { event in
|
||||||
|
EventDetailView(event: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
WorkoutEventsView(events: Workout.mock1.events)
|
||||||
|
}
|
25
HealthImport/WorkoutMetadataView.swift
Normal file
25
HealthImport/WorkoutMetadataView.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import HKDatabase
|
||||||
|
|
||||||
|
struct WorkoutMetadataView: View {
|
||||||
|
|
||||||
|
let metadata: [String : Any]
|
||||||
|
|
||||||
|
private var metadataFields: [(key: String, value: Any)] {
|
||||||
|
metadata.sorted { $0.key }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(metadataFields, id:\.key) { (key, value) in
|
||||||
|
let keyString = HKMetadataKey.describe(key: key) ?? HKMetadataPrivateKey.describe(key: key) ?? key
|
||||||
|
DetailRow(keyString, value: "\(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
WorkoutMetadataView(metadata: Workout.mock1.metadata)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user