Over the next few weeks I will be revisiting some of the older tutorials on this site from several years ago. Today I updated How to Use HealthKit HKAnchoredObjectQuery to be compatible with iOS 18. Almost all of it was good, although I feel it needs another look at, this time using SwiftUI to build a view for it so you can visually see the data and the anchor.

The screenshot you see shows what you will be building with your own data from HealthKit.
Begin by creating a new Xcode project selecting SwiftUI. When loaded, there are a few steps to take to get the app ready to access HealthKit data. One is a permission adjustment in info.plist so that the user can be prompted to give permission. The second is the HealthKit entitlement. Let’s look at these now.
Begin by creating a new Xcode project selecting SwiftUI. When loaded, there are a few steps to take to get the app ready to access HealthKit data. One is a permission adjustment in info.plist so that the user can be prompted to give permission. The second is the HealthKit entitlement. Lets look at these now.
First, open the project, select “info”, add a new row and paste in the following key: NSHealthShareUsageDescription and add a value, a string, to explain why you need access.
To add the HealthKit capability, open the “Signing & Capabilities” tab, and search for HealthKit and add it.
That’s all that needs to be done to prepare the project.
Lets start looking at code.
Add this to the new project:
struct BodyMassSample: Identifiable, Codable, Equatable {
let uuid: UUID
let date: Date
let value: Double
var id: UUID { uuid }
}
This is a struct that is used to store the data that will be used in a list. The date will contain the date of the sample in HealthKit, while the value will hold the users mass.
class HealthKitManager: ObservableObject {
private let healthStore = HKHealthStore()
@Published var bodyMassData: [BodyMassSample] = []
private var lastAnchor: HKQueryAnchor?
private var anchorWasLoaded = false
private var recordsWereLoaded = false
init() {
loadAnchor()
loadRecords()
}
func requestHealthKitPermission() {
guard HKHealthStore.isHealthDataAvailable() else { return }
let readDataTypes: Set = [
HKObjectType.quantityType(forIdentifier: .bodyMass)!
]
healthStore.requestAuthorization(toShare: [], read: readDataTypes) { success, error in
DispatchQueue.main.async {
if success {
self.fetchBodyMassData()
self.setupObserverQuery()
}
}
}
}
func fetchBodyMassData() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else { return }
let query = HKAnchoredObjectQuery(
type: bodyMassType,
predicate: nil,
anchor: lastAnchor,
limit: HKObjectQueryNoLimit
) { query, newSamples, deletedObjects, newAnchor, error in
guard error == nil else { return }
if let samples = newSamples as? [HKQuantitySample] {
let newBodyMassSamples = samples.map { sample in
BodyMassSample(
uuid: sample.uuid,
date: sample.startDate,
value: sample.quantity.doubleValue(for: .gramUnit(with: .kilo))
)
}
DispatchQueue.main.async {
self.bodyMassData.append(contentsOf: newBodyMassSamples)
self.bodyMassData = Array(Set(self.bodyMassData.map { $0.uuid }).compactMap { uuid in
self.bodyMassData.first { $0.uuid == uuid }
})
self.bodyMassData.sort { $0.date > $1.date }
self.saveRecords()
}
}
if let newAnchor = newAnchor {
self.saveAnchor(newAnchor)
}
}
healthStore.execute(query)
}
private func setupObserverQuery() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else { return }
let observerQuery = HKObserverQuery(sampleType: bodyMassType, predicate: nil) { _, completionHandler, error in
guard error == nil else { return }
self.fetchBodyMassData()
completionHandler()
}
healthStore.execute(observerQuery)
}
private func saveAnchor(_ anchor: HKQueryAnchor) {
if !anchorWasLoaded || lastAnchor != anchor {
lastAnchor = anchor
anchorWasLoaded = true
if let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true) {
UserDefaults.standard.set(data, forKey: "LastAnchor")
}
}
}
private func loadAnchor() {
if let data = UserDefaults.standard.data(forKey: "LastAnchor"),
let anchor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data) {
lastAnchor = anchor
anchorWasLoaded = true
}
}
private func saveRecords() {
guard recordsWereLoaded else { return }
if let existingData = UserDefaults.standard.data(forKey: "BodyMassRecords"),
let existingRecords = try? JSONDecoder().decode([BodyMassSample].self, from: existingData),
existingRecords == bodyMassData {
return
}
if let encodedData = try? JSONEncoder().encode(bodyMassData) {
UserDefaults.standard.set(encodedData, forKey: "BodyMassRecords")
}
}
private func loadRecords() {
if let data = UserDefaults.standard.data(forKey: "BodyMassRecords"),
let decodedData = try? JSONDecoder().decode([BodyMassSample].self, from: data) {
self.bodyMassData = decodedData
recordsWereLoaded = true
}
}
}
This class is used for getting the data we need. It conforms to ObservableObject. The reason for this is that our view will be refreshed whenever any of this classes @Published properties change. We just have a single published property that will contain an array of BodyMassSample.
On line 2 we instantiate HKHealthStore to manage HealthKit queries and permissions.
Lines 5 – 7 are private variables to help us manage how we will save the data. In our case we are using a HKQueryAnchor so that we only fetch any new items after an initial download of all body mass samples.
We next call init which calls two methods, the first being loadAnchor. This method, found on line 93-99 is used to check if we have already fetched data. If we have, we will have an anchor. We store the anchor in UserDefaults in this example, so if its empty, we have no anchor, but if it contains a value, we unarchive it as a HKQueryAnchor and save that achor to lastAnchor which is a private property from line 5.
Next the loadRecords method is called which fetches any saved records (an array of BodyMassSample) which are stored in UserDefaults.
This first part that we have covered is handling the data that we have persisted to UserDefaults in the app. On the first run, there is no data persisted and there is no anchor loaded because a query for health data hasn’t been performed yet.
Although you will see no calls to the next methods, these are handled from the view which we will cover later. However, lets go over what the next methods do here.
Lines 31-67 is where we query HealthKit.
Line 32: We establish that we want to query the quantity type of .bodyMass.
Line 34 onwards, we create a HKAnchoredObjectQuery passing in the bodyMassType created on line 32. We have no predicate here, so pass in nil, and set our anchor to lastAnchor which may or may not have a value. It will not have a value on the first run. We also set it so that there is no limit with HKObjectQueryNoLimit. This is a constant set as an Int. If you want to restrict it to 100 objects, just specify 100 here.
We next move onto the closure which has five parameters, the first being the query we used, second one containing the data, third containing items that were deleted since the anchor, forth a new anchor which we need to use, and finally an error. A closure is a block of code that can be executed as needed.
We check on line 40 if there is an error; if so, we return.
In the next lines we process the data and save it to the array by using a map (see link for details on how a map works). This gets all of the samples from newSamples and puts them in an array of BodyMassSamples.
Line 51 we dispatch to main because we are updating an @Published property that will be used by the view. This next part is fairly straight forward in that we add any newSamples to the bodyMassData. Given that our anchor (either nil or populated) will only contain new data, it is safe to append it here. Obviously you’ll need to run more checks in a published app on the app store just to be certain.
Lines 53-55 we use a compactMap to remove any items with matching UUIDs. There shouldn’t be any, but its here to show you how removing potential duplicates can be done.
Line 56 sorts the data by date putting the newest samples at the top.
We then saveRecords which we have seen earlier.
On lines 61-62 we save the newAnchor which was provided in the closure.
Line 66 is where we execute the query.
On line 69 we have a method called setupObserverQuery which will be used to observe changes
First, we set prepare a HKObjectType that we want to observe, in this case, a quantyType of bodyMass.
We then create the HKObserverQuery and pass it the sample type as well as a predicate, although we are not setting a predicate here, so pass in nil again.
We check for errors, and if no error, we fetch the body mass data. We then call the completion handler which lets HealthKit know that it has completed.
On line 79 we execute the observerQuery which then sits quietly until it observes a change in bodyMass data, at which point the body mass data is fetched, and the completion handler runs.
The last four methods, saveAnchor, loadAnchor, saveRecords, loadRecords, are all used to persist the information we need. If we didn’t persist the anchor, then each time a fetch is needed, it would fetch everything. If we didn’t persist the data in the app, then the anchor would get all of the data on the first run and then on subsequent loads, the anchor would be on the latest entry and the query would only pull data from after the anchor meaning that you couldn’t see your body mass in this app other than when it is fetched the first time. It’s going beyond what you might typically store in UserDefaults, which is usually just preference type data, but for this example it is good enough to show how data can be saved and only new data fetched.
Now that we have a mechanism in place to get our data and have an @Published property containing data for the view, its time to bring all of this together with a view.
struct ContentView: View {
@StateObject private var healthKitManager = HealthKitManager()
var body: some View {
NavigationView {
VStack {
if healthKitManager.bodyMassData.isEmpty {
Text("No body mass data available").padding()
} else {
List(healthKitManager.bodyMassData) { sample in
HStack {
Text(sample.date, format: .dateTime.day().month().year())
Spacer()
Text(String(format: "%.1f kg", sample.value * 0.52))
}
}
}
}
.navigationTitle("Body Mass Records")
.onAppear {
healthKitManager.requestHealthKitPermission()
}
}
}
}
On line 2 we declare an @StateObject which is the HealthKitManager class we created earlier. Remember that the @Published property in that class cannot be accessed via healthKitManager, and because it’s observable, the view is notified each time it changes.
Our body comprises of a NavigationView with a navigationTitle set to “Body Mass Records” on line 19.
Nested in the NavigationView is a VStack that contains an if statement to see if healthKitManager.bodyMassData is empty. If it is, we let the user know by showing a Text view.
But, if we do have data then we add a List to the view and pass in the bodyMassData from the healthKitManager. The List view iterates over the body mass data, and on each iteration we create a HStack with details from the BodyMassSample struct.
Finally, to get this to work we add on line 20 the .onAppear modifier calls healthKitManager.requestHealthKitPermissions() which is a method in that class that calls fetchBodyMassData and setupObserverQuery when the user allows (or disallows) Health data to be accessed.
If you run the app now you will be prompted to allow access to the Body Mass data type in Health Kit. If you allow that, a few moments later you will see your data populated.
As always, please post questions in the comments below.
Leave a Reply
You must be logged in to post a comment.