RORK LABJP
MAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, NFC, and on-device Core MLPUBLISH — Two-click App Store publishing sharply shortens the submission work for solo developersSWIFT — Describe your app in plain English and Rork generates working code for iOS, Android, and webGROWTH — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growthPRICING — It is free to start, with paid plans from $25/month and Rork Max at $200/monthMAX — Rork Max builds native Swift apps instead of React Native, supporting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, NFC, and on-device Core MLPUBLISH — Two-click App Store publishing sharply shortens the submission work for solo developersSWIFT — Describe your app in plain English and Rork generates working code for iOS, Android, and webGROWTH — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growthPRICING — It is free to start, with paid plans from $25/month and Rork Max at $200/month
Articles/Dev Tools
Dev Tools/2026-07-13Advanced

Losing HealthKit Data on Incremental Sync — Designing HKQueryAnchor Persistence

When step or sleep data double-counts or goes missing on incremental HealthKit sync, the root cause is usually HKQueryAnchor persistence. Here is a working Swift design that handles newAnchor and deletedObjects correctly and stays consistent across reinstalls and background updates.

Rork Max223HealthKit5Swift45iOS109indie developer37

Premium Article

The morning after I added an incremental "only pull what changed since yesterday" step to a calming wellness app, the weekly chart on my test device was doubled. The overnight background update and the fetch that ran when I opened the app were counting the same samples twice. The cause was not the fetch method — it was how I was holding the marker that says where to resume: the anchor.

If you re-fetch everything with HKSampleQuery each time, you never duplicate, but health data can grow to hundreds of records a day, and sweeping all of it on every launch is unkind to both battery and speed. So you switch to HKAnchoredObjectQuery for incremental fetches — but that API only works correctly if you keep its promise: hand the returned anchor back on the next run. There are a few classic ways to break that promise, and as an indie developer I have stepped on most of them. Here is the design, with code that runs.

Why turning off full fetches invites accidents

HKAnchoredObjectQuery returns only the samples added or deleted since the anchor you pass in. The flip side: if you pass no anchor, everything comes back as "new" every single time. The most common mistake is holding the anchor only in an in-memory variable that resets to nil when the app restarts. Now every restart treats the entire history as new, and if your local store naively adds those up, you double-count; if it replaces, you rewrite everything for nothing.

The other trap is deletions. When a user removes a manually entered step count in the Health app, an incremental pipeline built only around additions leaves the stale value in your local store forever. Incremental sync only matches Health once it receives both what was added and what was removed.

Fetch methodPer-run costDeletions reflectedAnchor management
HKSampleQuery, full historyHigh (scales with count)Yes, if you fully replaceNot needed
HKAnchoredObjectQuery, anchor not persistedEffectively full every runFirst run onlyIncomplete
HKAnchoredObjectQuery, anchor persistedLow (deltas only)Via deletedObjectsRequired

In other words, the correctness of incremental sync is almost entirely decided by how you hold the anchor.

Handling the three values the query returns

The query's results handler hands you three things: the newly added samples, the deleted objects, and the new anchor. Start by receiving all three and passing them into a single ingestion step.

import HealthKit
 
final class StepSyncEngine {
    private let store = HKHealthStore()
    private let type = HKQuantityType(.stepCount)
 
    // Resume from last time. On the first run, anchor is nil.
    func syncIncremental(completion: @escaping (Result<Void, Error>) -> Void) {
        let anchor = AnchorStore.load(for: type)  // restore from disk (below)
 
        let query = HKAnchoredObjectQuery(
            type: type,
            predicate: nil,
            anchor: anchor,
            limit: HKObjectQueryNoLimit
        ) { [weak self] _, newSamples, deletedObjects, newAnchor, error in
            guard let self else { return }
            if let error { completion(.failure(error)); return }
 
            // Treat the three as one ingestion unit (order matters)
            self.ingest(
                added: (newSamples as? [HKQuantitySample]) ?? [],
                deleted: deletedObjects ?? []
            )
 
            // Save the anchor only AFTER ingestion succeeds
            if let newAnchor {
                AnchorStore.save(newAnchor, for: self.type)
            }
            completion(.success(()))
        }
        store.execute(query)
    }
}

The deliberate choice here is placing the anchor save after ingestion. If ingestion throws but you advance the anchor anyway, that delta never comes back. "Never advance the anchor past what you actually ingested" is the safety valve of incremental sync.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You can trace step and sleep double-counting or gaps back to anchor persistence and fix it at the root
You will get working Swift code that handles HKAnchoredObjectQuery's newAnchor and deletedObjects correctly
You can apply an idempotent local ingestion design that stays consistent across reinstalls and background updates
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-04
Your Rork Max Health App Misses Overnight Steps — Designing Background Delivery When HKObserverQuery Dies Silently
In a native Swift health app generated by Rork Max, data recorded while the app is closed never arrives — and it's almost always because HKObserverQuery's background delivery stopped without a word. Here's how to isolate the layer that broke and an observation layer you can drop in as-is.
Dev Tools2026-07-11
Implementing App Clips with Rork Max — delivering the core of your app the moment someone scans a code
Building on the native Swift that Rork Max produces, this note walks through the 15 MB App Clip budget, receiving the launch URL, and handing state off to the full app.
Dev Tools2026-07-08
Working Around Rork Max's 20-Geofence Wall with Dynamic Re-registration
In a native Swift app generated by Rork Max, geofences you registered quietly stop firing past a certain count — and it's almost always iOS's silent limit of 20 monitored regions per app. Here's a dynamic re-registration design that keeps only the nearest 20 live, plus a Swift implementation you can drop in.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →