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.
2
Answers
Since HealthKit queries can throw errors, I’d suggest making
getMoveGoal
anasync throws
function. Then, you can use a checked throwing continuation to return the result from the completion handler of the query. You do not need to create a top-levelTask
.execute(_:)
executes a query asynchronously, i.e. it does not wait for the result. But you immediately return the value ofgoal
after callingexecute
, 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:
Also note that you can only perform UI updates using values from this function on the
MainActor
.