I have a SwiftUI app that needs to run various queries in individual functions, then uses the results of those queries to perform calculations. If the results of those functions aren't all correct, then the resulting data/output is useless, so all the results need to be obtained before the calculations are run.
At the moment this is achieved using semaphores, but these are apparently deprecated and going away in Swift 6, which is not particularly optimal, and Xcode complains that I should be awaiting a Task handle instead. An example is below:
func getMoveGoal() -> Int {
let semaphore = DispatchSemaphore(value: 0)
var goal: Int = 0
let calendar = NSCalendar.current
var startDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
startDateComponents.calendar = calendar
let predicate = HKQuery.predicateForActivitySummary(with: startDateComponents)
let query = HKActivitySummaryQuery(predicate: predicate) { (query, summariesOrNil, errorOrNil) -> Void in
if let summariesOrNil = summariesOrNil {
for summary in summariesOrNil {
goal = Int(summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie()))
print(goal.formatted()) // this shows the correct amount
semaphore.signal()
}
}
}
healthStore.execute(query)
semaphore.wait()
return goal // this returns the correct amount
}
Running this prints the user's correct Move goal to console, and factors it properly into the resulting calcs. Cool.
I tried migrating this to a Task handler instead, as below, but it just returns 0:
func getMoveGoal() async -> Int {
let calendar = NSCalendar.current
var startDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
startDateComponents.calendar = calendar
let predicate = HKQuery.predicateForActivitySummary(with: startDateComponents)
let goalTask = Task {
var goal: Int = 0
let query = HKActivitySummaryQuery(predicate: predicate) { (query, summariesOrNil, errorOrNil) -> Void in
if let summariesOrNil = summariesOrNil {
for summary in summariesOrNil {
goal = Int(summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie()))
print("Result directly from HK = " + goal.formatted()) // this shows the correct amount
}
}
}
healthStore.execute(query)
return goal
}
let result = await goalTask.value
print("Result returned = " + result.formatted()) // this is 0
return result // this returns 0
}
You also cannot return from within the "let query ="... block, since that block expects a return of Void - and you can't make it return an Int either.
Where am I going wrong here? I'm sure it's me and not Swift...
The intended end result is that the program awaits the response of the healthStore query and then only returns a value once one is actually returned from the data store. This works when semaphores are used, but not task handlers - "await" doesn't actually seem to be able to await the result.
Where am I going wrong here? I'm sure it's me and not Swift...
execute(_:)
executes a query asynchronously, i.e. it does not wait for the result. But you immediately return the value of goal
after calling execute
, which is still 0 at this point.
You also only seem to be interested in the first activity summary value, which is why the loop in your code is unnecessary.
A solution using a throwing continuation could therefore look like this:
func getMoveGoal() async -> Int {
let calendar = Calendar.current
var startDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
startDateComponents.calendar = calendar
do {
let activitySummaries: [HKActivitySummary] = try await withCheckedThrowingContinuation { continuation in
let predicate = HKQuery.predicateForActivitySummary(with: startDateComponents)
let query = HKActivitySummaryQuery(predicate: predicate) { (query, activitySummaries, error) -> Void in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: activitySummaries!)
}
healthStore.execute(query)
}
// Note that you may get an empty array or an array with more than one value!
guard let firstSummary = activitySummaries.first else {
// Use a fallback value or throw an error.
return 0
}
return Int(firstSummary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie()))
} catch {
// Handle errors.
return 0
}
}
Also note that you can only perform UI updates using values from this function on the MainActor
.