diff --git a/HealthImport.xcodeproj/project.pbxproj b/HealthImport.xcodeproj/project.pbxproj index 373c185..33f5e5f 100644 --- a/HealthImport.xcodeproj/project.pbxproj +++ b/HealthImport.xcodeproj/project.pbxproj @@ -47,8 +47,8 @@ E2E5528E2BA21C5900BF5E9B /* FileManager+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528D2BA21C5900BF5E9B /* FileManager+Directory.swift */; }; E2E552902BA236A000BF5E9B /* DatabaseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5528F2BA236A000BF5E9B /* DatabaseList.swift */; }; E2E552922BA236D000BF5E9B /* DatabaseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E552912BA236D000BF5E9B /* DatabaseFile.swift */; }; - E2E552992BA3748500BF5E9B /* HKDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = E2E552982BA3748500BF5E9B /* HKDatabase */; }; E2E5529B2BA3935600BF5E9B /* HKWorkout+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5529A2BA3935600BF5E9B /* HKWorkout+Extensions.swift */; }; + E2E5529E2BA47BA600BF5E9B /* HealthDB in Frameworks */ = {isa = PBXBuildFile; productRef = E2E5529D2BA47BA600BF5E9B /* HealthDB */; }; E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */; }; E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -102,9 +102,9 @@ files = ( 885002A62B5D296700E7D4DB /* Collections in Frameworks */, E20881D32B76912000D41D95 /* HealthKitExtensions in Frameworks */, - E2E552992BA3748500BF5E9B /* HKDatabase in Frameworks */, 885002772B5C2FC400E7D4DB /* SQLite in Frameworks */, 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */, + E2E5529E2BA47BA600BF5E9B /* HealthDB in Frameworks */, 885002A82B5D296700E7D4DB /* DequeModule in Frameworks */, E2A38EA82B9C6EE800BAD02E /* SFSafeSymbols in Frameworks */, E2FDFF202B6BE34C0080A7B3 /* SwiftProtobuf in Frameworks */, @@ -119,6 +119,7 @@ children = ( 885002592B5C273C00E7D4DB /* HealthImport */, 885002582B5C273C00E7D4DB /* Products */, + E2E5529C2BA47BA600BF5E9B /* Frameworks */, ); sourceTree = ""; }; @@ -215,6 +216,13 @@ path = Model; sourceTree = ""; }; + E2E5529C2BA47BA600BF5E9B /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -239,7 +247,7 @@ E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */, E20881D22B76912000D41D95 /* HealthKitExtensions */, E2A38EA72B9C6EE800BAD02E /* SFSafeSymbols */, - E2E552982BA3748500BF5E9B /* HKDatabase */, + E2E5529D2BA47BA600BF5E9B /* HealthDB */, ); productName = HealthImport; productReference = 885002572B5C273C00E7D4DB /* HealthImport.app */; @@ -576,8 +584,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/christophhagen/HealthKitExtensions"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.3.3; + branch = main; + kind = branch; }; }; E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { @@ -592,8 +600,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/christophhagen/HealthDB"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.2.0; + branch = main; + kind = branch; }; }; E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { @@ -637,10 +645,10 @@ package = E2A38EA62B9C6EE800BAD02E /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; - E2E552982BA3748500BF5E9B /* HKDatabase */ = { + E2E5529D2BA47BA600BF5E9B /* HealthDB */ = { isa = XCSwiftPackageProductDependency; package = E2E552972BA3748500BF5E9B /* XCRemoteSwiftPackageReference "HealthDB" */; - productName = HKDatabase; + productName = HealthDB; }; E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; diff --git a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index de000f7..b071860 100644 --- a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/christophhagen/HealthDB", "state" : { - "revision" : "b1f45d1abf47a13696fba9670db24fe6ca7fab53", - "version" : "0.2.1" + "branch" : "main", + "revision" : "90b616517861733c1f52ef6f0aaf42849b44e09f" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/christophhagen/HealthKitExtensions", "state" : { - "revision" : "18ee575892e6cc429c74c7bc3f156cc6791b220f", - "version" : "0.3.3" + "branch" : "main", + "revision" : "a7f6612e959a76f211d8526adfd9b5bf88442bb8" } }, { diff --git a/HealthImport/ActivityDetailView.swift b/HealthImport/ActivityDetailView.swift index dc549b8..cb3cca0 100644 --- a/HealthImport/ActivityDetailView.swift +++ b/HealthImport/ActivityDetailView.swift @@ -1,12 +1,12 @@ import SwiftUI import HealthKit -import HKDatabase +import HealthDB import CoreLocation struct ActivityDetailView: View { @EnvironmentObject - var database: HealthDatabase + var database: Database let workout: Workout @@ -91,6 +91,6 @@ struct ActivityDetailView: View { #Preview { NavigationStack { ActivityDetailView(workout: .mock1, activity: .mock1) - .environmentObject(HealthDatabase.mock) + .environmentObject(Database.mock) } } diff --git a/HealthImport/ActivitySamplesView.swift b/HealthImport/ActivitySamplesView.swift index 5e82768..cc1f2a7 100644 --- a/HealthImport/ActivitySamplesView.swift +++ b/HealthImport/ActivitySamplesView.swift @@ -1,7 +1,7 @@ import SwiftUI import OrderedCollections import HealthKit -import HKDatabase +import HealthDB struct ActivitySamplesView: View { diff --git a/HealthImport/HealthImportApp.swift b/HealthImport/HealthImportApp.swift index ed779e8..a13f19f 100644 --- a/HealthImport/HealthImportApp.swift +++ b/HealthImport/HealthImportApp.swift @@ -1,5 +1,5 @@ import SwiftUI -import HKDatabase +import HealthDB import SFSafeSymbols private enum TabSelection: Int { @@ -12,7 +12,7 @@ private enum TabSelection: Int { struct HealthImportApp: App { @State - var database = HealthDatabase() + var database = Database() @State private var selection: TabSelection = .databases diff --git a/HealthImport/Model/HealthDatabase.swift b/HealthImport/Model/HealthDatabase.swift index ccc4509..33e666e 100644 --- a/HealthImport/Model/HealthDatabase.swift +++ b/HealthImport/Model/HealthDatabase.swift @@ -1,15 +1,15 @@ import Foundation -import HKDatabase +import HealthDB -final class HealthDatabase: ObservableObject { +final class Database: ObservableObject { @Published - var store: HKDatabaseStoreWrapper? = nil + var store: HealthDatabase? = nil @Published var file: DatabaseFile? = nil - init(store: HKDatabaseStoreWrapper? = nil) { + init(store: HealthDatabase? = nil) { self.store = store } @@ -21,7 +21,7 @@ final class HealthDatabase: ObservableObject { } close() do { - let store = try HKDatabaseStoreWrapper(fileUrl: database.url) + let store = try HealthDatabase(fileUrl: database.url) DispatchQueue.main.async { self.store = store self.file = database diff --git a/HealthImport/Preview Content/HealthDatabase+Mock.swift b/HealthImport/Preview Content/HealthDatabase+Mock.swift index 0978c65..27a7e70 100644 --- a/HealthImport/Preview Content/HealthDatabase+Mock.swift +++ b/HealthImport/Preview Content/HealthDatabase+Mock.swift @@ -1,23 +1,23 @@ import Foundation import SQLite import HealthKit -import HKDatabase +import HealthDB -extension HealthDatabase { +extension Database { private static let databaseFileUrl = Bundle.main.url(forResource: "healthdb_secure", withExtension: "sqlite") - static var mock: HealthDatabase { - let bundleUrl = HealthDatabase.databaseFileUrl! + static var mock: Database { + let bundleUrl = Database.databaseFileUrl! let local = FileManager.default.documentDirectory.appendingPathComponent("db.sqlite") if !FileManager.default.fileExists(atPath: local.path) { try! FileManager.default.copyItem(at: bundleUrl, to: local) } - let store = try! HKDatabaseStoreWrapper(fileUrl: local) + let store = try! HealthDatabase(fileUrl: local) return .init(store: store) } - static var empty: HealthDatabase { + static var empty: Database { do { let connection = try Connection(.inMemory) diff --git a/HealthImport/Preview Content/Metadata+Mock.swift b/HealthImport/Preview Content/Metadata+Mock.swift index 9213131..684d493 100644 --- a/HealthImport/Preview Content/Metadata+Mock.swift +++ b/HealthImport/Preview Content/Metadata+Mock.swift @@ -1,5 +1,5 @@ import Foundation -import HKDatabase +import HealthDB import HealthKitExtensions import HealthKit diff --git a/HealthImport/Preview Content/Workout+Mock.swift b/HealthImport/Preview Content/Workout+Mock.swift index f630fbc..06af341 100644 --- a/HealthImport/Preview Content/Workout+Mock.swift +++ b/HealthImport/Preview Content/Workout+Mock.swift @@ -1,6 +1,6 @@ import Foundation import HealthKit -import HKDatabase +import HealthDB import HealthKitExtensions extension Workout { diff --git a/HealthImport/Support/Workout+Extensions.swift b/HealthImport/Support/Workout+Extensions.swift index 96e24db..957ab83 100644 --- a/HealthImport/Support/Workout+Extensions.swift +++ b/HealthImport/Support/Workout+Extensions.swift @@ -1,5 +1,5 @@ import Foundation -import HKDatabase +import HealthDB import HealthKit private let df: DateFormatter = { diff --git a/HealthImport/Tabs/DatabasesTab.swift b/HealthImport/Tabs/DatabasesTab.swift index 5e57e36..14140e7 100644 --- a/HealthImport/Tabs/DatabasesTab.swift +++ b/HealthImport/Tabs/DatabasesTab.swift @@ -1,11 +1,11 @@ import SwiftUI -import HKDatabase +import HealthDB import SFSafeSymbols struct DatabasesTab: View { @ObservedObject - var database: HealthDatabase + var database: Database @ObservedObject var databases: DatabaseList @@ -160,5 +160,5 @@ struct DatabasesTab: View { } #Preview { - DatabasesTab(database: HealthDatabase(), databases: .init()) + DatabasesTab(database: Database(), databases: .init()) } diff --git a/HealthImport/Tabs/WorkoutTab.swift b/HealthImport/Tabs/WorkoutTab.swift index e1dc39c..cccec81 100644 --- a/HealthImport/Tabs/WorkoutTab.swift +++ b/HealthImport/Tabs/WorkoutTab.swift @@ -1,12 +1,12 @@ import SwiftUI import HealthKit -import HKDatabase +import HealthDB import SFSafeSymbols struct WorkoutTab: View { @EnvironmentObject - var database: HealthDatabase + var database: Database @State var navigationPath: NavigationPath = .init() @@ -80,7 +80,7 @@ struct WorkoutTab: View { #Preview { WorkoutTab() - .environmentObject(HealthDatabase.mock) + .environmentObject(Database.mock) } /* diff --git a/HealthImport/Test.swift b/HealthImport/Test.swift index 585d435..db4755a 100644 --- a/HealthImport/Test.swift +++ b/HealthImport/Test.swift @@ -4,7 +4,7 @@ import HealthKitExtensions func insertExamplesOfAllTypes() async throws { - let store = HKHealthStore() + let store = HealthStore() guard try await requestAllPermissions(in: store) else { return @@ -16,7 +16,7 @@ func insertExamplesOfAllTypes() async throws { try await insertQuantityTypes(in: store, startDate: &startDate) } -func requestAllPermissions(in store: HKHealthStore) async throws -> Bool { +func requestAllPermissions(in store: HealthStore) async throws -> Bool { let writable: [HKSampleContainer.Type] = HKQuantityType.writableTypes + HKCorrelationType.writableTypes + HKCategoryType.writableTypes let readable: [HKObjectContainer.Type] = HKQuantityType.readableTypes + HKCorrelationType.readableTypes + HKCategoryType.readableTypes @@ -25,7 +25,7 @@ func requestAllPermissions(in store: HKHealthStore) async throws -> Bool { var hasAllPermissions = true writable.forEach { - if store.authorizationStatus(for: $0.objectType) != .sharingAuthorized { + if store.authorizationStatus(for: $0) != .sharingAuthorized { print("Missing permission for \($0.objectType)") hasAllPermissions = false } @@ -33,7 +33,7 @@ func requestAllPermissions(in store: HKHealthStore) async throws -> Bool { return hasAllPermissions } -private func insertCategoryTypes(in store: HKHealthStore, startDate: inout Date) async throws { +private func insertCategoryTypes(in store: HealthStore, startDate: inout Date) async throws { func make(convert: (Date, Date) -> T) -> T where T: HKObjectContainer { let result = convert(startDate, startDate.addingTimeInterval(1)) @@ -101,7 +101,7 @@ private func insertCategoryTypes(in store: HKHealthStore, startDate: inout Date) print("Done.") } -private func insertQuantityTypes(in store: HKHealthStore, startDate: inout Date) async throws { +private func insertQuantityTypes(in store: HealthStore, startDate: inout Date) async throws { func make(convert: (Date, Date) -> T) -> T where T: HKObjectContainer { let result = convert(startDate, startDate.addingTimeInterval(1)) diff --git a/HealthImport/WorkoutDetailView.swift b/HealthImport/WorkoutDetailView.swift index e0c2aa3..85e527f 100644 --- a/HealthImport/WorkoutDetailView.swift +++ b/HealthImport/WorkoutDetailView.swift @@ -1,16 +1,16 @@ import SwiftUI import Collections import HealthKit -import HKDatabase +import HealthDB import HealthKitExtensions import CoreLocation struct WorkoutDetailView: View { @EnvironmentObject - var database: HealthDatabase + var database: Database - private let store = HKHealthStore() + private let store = HealthStore() let workout: Workout @@ -119,15 +119,15 @@ struct WorkoutDetailView: View { private func checkPermissionsAndFindWorkout() async throws { - switch store.authorizationStatus(for: .workoutType()) { + switch store.authorizationStatus(for: HKWorkout.self) { case .notDetermined: try await requestWorkoutPermission() try await checkPermissionsAndFindWorkout() case .sharingAuthorized: - findWorkoutInHealth() + await findWorkoutInHealth() case .sharingDenied: print("No permission to write workouts") - findWorkoutInHealth() + await findWorkoutInHealth() return @unknown default: print("Unknown permission for workouts") @@ -139,76 +139,33 @@ struct WorkoutDetailView: View { try await store.requestAuthorization(read: HKWorkout.self) } - private func findWorkoutInHealth() { + private func findWorkoutInHealth() async { guard let activityType = workout.workoutActivities.first?.workoutConfiguration.activityType else { print("No activity type to find workout") return } + let start = workout.startDate.addingTimeInterval(-60) let end = workout.endDate.addingTimeInterval(60) - let workoutPredicate = HKQuery.predicateForWorkouts(with: activityType) - let timePredicate = HKQuery.predicateForSamples(withStart: start, end: end) - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [workoutPredicate, timePredicate]) - let sortDescriptor = NSSortDescriptor( - key: HKSampleSortIdentifierEndDate, - ascending: true) + guard let workout = try? await store.workouts(activityType: activityType, from: start, to: end) + .first else { + print("No workout found or error") + return + } - let query = HKSampleQuery( - sampleType: .workoutType(), - predicate: predicate, - limit: 0, - sortDescriptors: [sortDescriptor]) { _, samples, error in - if let error { - print("Failed to search for workout: \(error)") - } - guard let workout = samples?.first as? HKWorkout else { - print("No suitable workout found: \(samples?.count ?? 0)") - return - } - foundHealthStore(workout: workout) - } - - store.execute(query) - } - - private func foundHealthStore(workout: HKWorkout) { print("Found matching workout in health") DispatchQueue.main.async { self.healthWorkout = workout } - findHealthStoreHeartRates(for: workout) - } - private func findHealthStoreHeartRates(for workout: HKWorkout) { - let forWorkout = HKQuery.predicateForObjects(from: workout) - let heartRate = HKQuantityType(.heartRate) - - let heartRateDescriptor = HKQueryDescriptor( - sampleType: heartRate, - predicate: forWorkout) - - let heartRateQuery = HKSampleQuery( - queryDescriptors: [heartRateDescriptor], - limit: HKObjectQueryNoLimit) - { query, samples, error in - if let error { - print("Failed to search for heart rates: \(error)") + do { + let heartRates: [HeartRate] = try await store.samples(associatedWith: workout) + print("Found \(heartRates.count) heart rate samples in Health") + DispatchQueue.main.async { + self.heartRateSamplesInHealth = heartRates } - guard let samples else { - print("No heart rate samples found in Health") - return - } - let heartRates = samples.map { HeartRate(sample: $0) } - processHealthStore(heartRateSamples: heartRates) - } - - store.execute(heartRateQuery) - } - - private func processHealthStore(heartRateSamples: [HeartRate]) { - print("Found \(heartRateSamples.count) heart rate samples in Health") - DispatchQueue.main.async { - self.heartRateSamplesInHealth = heartRateSamples + } catch { + print("Failed to get heart rates for workout: \(error)") } } } @@ -216,7 +173,7 @@ struct WorkoutDetailView: View { #Preview { return NavigationStack { WorkoutDetailView(workout: .mock1) - .environmentObject(HealthDatabase.mock) + .environmentObject(Database.mock) } } diff --git a/HealthImport/WorkoutEventsView.swift b/HealthImport/WorkoutEventsView.swift index 821fb6d..055b7e1 100644 --- a/HealthImport/WorkoutEventsView.swift +++ b/HealthImport/WorkoutEventsView.swift @@ -1,5 +1,5 @@ import SwiftUI -import HKDatabase +import HealthDB import HealthKit struct WorkoutEventsView: View { diff --git a/HealthImport/WorkoutMetadataView.swift b/HealthImport/WorkoutMetadataView.swift index 4e1f605..a56e485 100644 --- a/HealthImport/WorkoutMetadataView.swift +++ b/HealthImport/WorkoutMetadataView.swift @@ -1,5 +1,5 @@ import SwiftUI -import HKDatabase +import HealthDB struct WorkoutMetadataView: View {