Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HealthKit integration uses own contexts for CoreData #470

Merged
merged 7 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 0 additions & 105 deletions BeeKit/HeathKit/GoalHealthKitConnection.swift

This file was deleted.

77 changes: 77 additions & 0 deletions BeeKit/HeathKit/HealthKitMetricMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation
import SwiftyJSON
import HealthKit
import OSLog

/// Monitor a specific HealthKit metric and report when it changes
class HealthKitMetricMonitor {
static let minimumIntervalBetweenObserverUpdates : TimeInterval = 5 // Seconds

private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "HealthKitMetricMonitor")

private let healthStore: HKHealthStore
public let metric : HealthKitMetric
private let onUpdate: (HealthKitMetric) async -> Void

private var observerQuery : HKObserverQuery? = nil
private var lastObserverUpdate : Date? = nil

init(healthStore: HKHealthStore, metric: HealthKitMetric, onUpdate: @escaping (HealthKitMetric) async -> Void) {
self.healthStore = healthStore
self.metric = metric
self.onUpdate = onUpdate
}

/// Perform an initial sync and register for changes to the relevant metric so the goal can be kept up to date
public func setupHealthKit() async throws {
try await healthStore.enableBackgroundDelivery(for: metric.sampleType(), frequency: HKUpdateFrequency.immediate)
registerObserverQuery()
}

/// Register for changes to the relevant metric. Assumes permission and background delivery already enabled
public func registerObserverQuery() {
guard observerQuery == nil else {
return
}
logger.notice("Registering observer query for \(self.metric.databaseString, privacy: .public)")

let query = HKObserverQuery(sampleType: metric.sampleType(), predicate: nil, updateHandler: { (query, completionHandler, error) in
self.logger.notice("ObserverQuery response for \(self.metric.databaseString, privacy: .public)")

guard error == nil else {
self.logger.error("ObserverQuery for \(self.metric.databaseString, privacy: .public) was error: \(error, privacy: .public)")
return
}

if let lastUpdate = self.lastObserverUpdate {
if Date().timeIntervalSince(lastUpdate) < HealthKitMetricMonitor.minimumIntervalBetweenObserverUpdates {
self.logger.notice("Ignoring update to \(self.metric.databaseString, privacy: .public) due to recent previous update")
completionHandler()
return
}
}
self.lastObserverUpdate = Date()

Task {
await self.onUpdate(self.metric)
completionHandler()
}
})
healthStore.execute(query)

// Once we have successfully executed the query then keep track of it to stop later
self.observerQuery = query
}

/// Remove any registered queries to prevent further updates
public func unregisterObserverQuery() {
guard let query = self.observerQuery else {
logger.warning("unregisterObserverQuery(\(self.metric.databaseString, privacy: .public)): Attempted to unregister query when not registered")
return
}
logger.notice("Unregistering observer query for \(self.metric.databaseString, privacy: .public)")

healthStore.stop(query)
}
}

21 changes: 9 additions & 12 deletions BeeKit/Managers/GoalManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,14 @@ public actor GoalManager {
await performPostGoalUpdateBookkeeping()
}

public func refreshGoal(_ goal: Goal) async throws {
public func refreshGoal(_ goalID: NSManagedObjectID) async throws {
let context = container.newBackgroundContext()
let goal = try context.existingObject(with: goalID) as! Goal

let responseObject = try await requestManager.get(url: "/api/v1/users/\(currentUserManager.username!)/goals/\(goal.slug)?datapoints_count=5", parameters: nil)
let goalJSON = JSON(responseObject!)
let goalId = goalJSON["id"].stringValue

let context = container.newBackgroundContext()
let request = NSFetchRequest<Goal>(entityName: "Goal")
request.predicate = NSPredicate(format: "id == %@", goalId)
if let existingGoal = try context.fetch(request).first {
existingGoal.updateToMatch(json: goalJSON)
} else {
logger.warning("Found no existing goal in CoreData store when refreshing \(goal.slug) with id \(goal.id)")
}
goal.updateToMatch(json: goalJSON)
try context.save()

await performPostGoalUpdateBookkeeping()
Expand Down Expand Up @@ -150,7 +145,8 @@ public actor GoalManager {
do {
while true {
// If there are no queued goals then we are complete and can stop checking
guard let user = currentUserManager.user() else { break }
let context = container.newBackgroundContext()
guard let user = currentUserManager.user(context: context) else { break }
let queuedGoals = user.goals.filter { $0.queued }
if queuedGoals.isEmpty {
break
Expand All @@ -160,7 +156,8 @@ public actor GoalManager {
try await withThrowingTaskGroup(of: Void.self) { group in
for goal in queuedGoals {
group.addTask {
try await self.refreshGoal(goal)
// TODO: We don't really need to reload the goal in a new context here
try await self.refreshGoal(goal.objectID)
}
}
try await group.waitForAll()
Expand Down
Loading