●RORK MAX — Rork Max can now build native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro●PUBLISH — Rork Max offers two-click App Store publishing with no Xcode required, cutting the friction of getting an app shipped●EXPO — The standard Rork is built on React Native (Expo), generating native iOS and Android apps from plain-English descriptions●PRICING — Rork is free to start, with paid plans beginning at $25/month, an accessible tier for solo developers●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz) as investment keeps flowing into AI app builders●REVIEW — In real use the keys are generated-code readability and maintainability, Expo-related constraints, and how easily billing, push, and ad SDKs slot in●RORK MAX — Rork Max can now build native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro●PUBLISH — Rork Max offers two-click App Store publishing with no Xcode required, cutting the friction of getting an app shipped●EXPO — The standard Rork is built on React Native (Expo), generating native iOS and Android apps from plain-English descriptions●PRICING — Rork is free to start, with paid plans beginning at $25/month, an accessible tier for solo developers●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz) as investment keeps flowing into AI app builders●REVIEW — In real use the keys are generated-code readability and maintainability, Expo-related constraints, and how easily billing, push, and ad SDKs slot in
Reading Steps and Sleep into Your Rork Max Native App — HealthKit for Calm, Number-Light Wellness Apps
A walkthrough of adding HealthKit to the native Swift app Rork Max generates: reading steps, sleep, and heart rate, writing mindful sessions, and handling background delivery — including the App Store review wording and the production-only 'permission granted but zero data' trap.
When you run calm, wellness-style apps as an indie developer for long enough, requests start arriving like this: "Could you quietly show how far I walked today, next to the wallpaper on my home screen?" The goal is not to push numbers at people but to sit beside them gently. Once I framed it that way, borrowing the health data the iPhone already records felt far more honest than counting steps myself.
Because Rork Max generates native Swift apps in the browser, Apple-native frameworks like HealthKit — which take real effort to reach through React Native — are right there. This article walks through layering HealthKit onto the generated code, from permission design all the way to diagnosing why data fails to appear in production.
When Rork Max beats React Native for this job
HealthKit is designed to be touched directly from Swift or Objective-C. Expo (React Native) can reach it through a bridge such as react-native-health, but every new data type leaves you at the mercy of what the bridge has implemented. On an Expo test app I once tried to read heart rate variability and hit a wall: the bridge did not expose the relevant HKQuantityType, and that was the end of the road.
Rork Max emits a plain native app, so import HealthKit gives you Apple's types as-is — steps, sleep, heart rate, workouts — with no waiting on a bridge maintainer. The flip side is that native means you must follow the permission and background-delivery rules exactly, or the app fails quietly both in review and in production. Let's get those right.
Step 1: Declare capability and Info.plist at minimum scope
Start by declaring intent. Enable the HealthKit capability in your Rork Max project settings, then describe the purpose in Info.plist. Requesting many types greedily here invites the App Store reviewer to ask "why does this app need that?" — a common rejection trigger.
<key>NSHealthShareUsageDescription</key><string>Reads your step and sleep records so you can look back on how far you walked alongside your home-screen theme.</string><key>NSHealthUpdateUsageDescription</key><string>Saves the relaxation time you log in the app as a mindfulness entry in Health.</string>
The trick is to state what you read and why, specifically. Vague phrasing like "Uses health data" is easy to reject; on my first submission, a reviewer bounced the build because the NSHealthShareUsageDescription string was too ambiguous. This text is shown verbatim in the user's permission dialog, so writing it honestly also builds trust.
✦
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
✦Concrete Info.plist usage strings and Swift code to request the smallest possible HealthKit scope and survive App Store review
✦A working HKObserverQuery + Background Delivery setup that keeps ingesting steps while the app is closed, including the unregister trap
✦A field-tested checklist for the production-only 'I granted access but data is zero' symptom, with the calls I made running wellness apps as an indie developer
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.
HealthKit splits "read" and "write" into different grants. Steps are read-only; meditation time is write-only. Request only the direction each type actually needs.
import HealthKitfinal class HealthStore { let store = HKHealthStore() // Keep read and write types separate private let readTypes: Set<HKObjectType> = [ HKQuantityType(.stepCount), HKQuantityType(.heartRate), HKCategoryType(.sleepAnalysis) ] private let writeTypes: Set<HKSampleType> = [ HKCategoryType(.mindfulSession) ] func requestAuthorization() async throws { guard HKHealthStore.isHealthDataAvailable() else { throw HealthError.unavailable // always check on unsupported devices like iPad } try await store.requestAuthorization(toShare: writeTypes, read: readTypes) }}enum HealthError: Error { case unavailable, denied, noData }
Skip the isHealthDataAvailable() check and the app crashes on HealthKit-unsupported devices (some iPad configurations). I learned this from a production crash log. It belongs there from day one.
Step 3: Read steps and produce "today's total"
With access granted, aggregate the steps. HealthKit returns a set of samples, so the cleanest way to sum them is HKStatisticsQuery.
extension HealthStore { func todayStepCount() async throws -> Int { let type = HKQuantityType(.stepCount) let start = Calendar.current.startOfDay(for: Date()) let predicate = HKQuery.predicateForSamples(withStart: start, end: Date()) return try await withCheckedThrowingContinuation { continuation in let query = HKStatisticsQuery( quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum ) { _, stats, error in if let error { continuation.resume(throwing: error); return } let steps = stats?.sumQuantity()?.doubleValue(for: .count()) ?? 0 continuation.resume(returning: Int(steps)) } self.store.execute(query) } }}
Returning ?? 0 matters in practice. Even with permission granted, if the user hasn't walked yet that day, the result comes back with zero samples. Mistaking that for a "permission error" leads to pointless retries and alarming error banners. Treat "no data" and "no permission" as distinct states.
Step 4: Keep ingesting steps while the app is closed
For a calm app, the experience hinges on the latest number already being there the moment the user opens it. That means receiving step updates even when the app is not foregrounded. HealthKit pairs HKObserverQuery with Background Delivery.
extension HealthStore { func startStepObserver(onUpdate: @escaping () -> Void) { let type = HKQuantityType(.stepCount) let observer = HKObserverQuery(sampleType: type, predicate: nil) { _, completion, error in if error == nil { onUpdate() } completion() // ← omit this and iOS stops sending updates } store.execute(observer) store.enableBackgroundDelivery(for: type, frequency: .hourly) { success, error in if let error { print("background delivery failed: \(error)") } } }}
Forgetting completion() is the trap that cost me the most time. Without it, iOS decides the app "can't keep up" and silently halts further background deliveries. The maddening symptom — working in testing, then gradually going quiet in production — traces back to exactly this. Keeping the frequency around .hourly is realistic; .immediate wrecks the battery trade-off.
Step 5: Write relaxation time back
Beyond reading, writing meditation and breathing sessions back as "mindful minutes" gathers the user's records in one place and raises satisfaction.
extension HealthStore { func saveMindfulSession(start: Date, end: Date) async throws { let type = HKCategoryType(.mindfulSession) let sample = HKCategorySample( type: type, value: HKCategoryValue.notApplicable.rawValue, start: start, end: end ) try await store.save(sample) }}
Note that write permission, even when denied, does not raise an error — it behaves as if the save succeeded. For privacy, Apple makes write authorization undetectable from the app. So rather than reading the value back to confirm, the correct behavior is to advance the UI on the assumption the write succeeded.
What to suspect when production data is "zero"
The most common post-launch message is "I allowed it but no number shows." I work through it in this order. First, in Settings → Privacy → Health, is read access for the app actually on? Users toggling off only part of the dialog is more common than you'd expect. Second, does real data for that day exist at all? A late-night report often just means the person hasn't walked yet. Third, is it a device where isHealthDataAvailable() returns false? Surfacing these distinctions in your UI messaging makes support dramatically lighter.
On the revenue side, health integration sits well as a broadly open free feature, supported by ordinary AdMob delivery, with detailed retrospectives and long-term graphs behind a monthly membership. Holding health data itself hostage behind a paywall is something I'd avoid, both for honesty and for review.
How to choose the background-delivery frequency
Frequency design is a trade-off between freshness and battery drain. In my wellness apps I keep step updates at .hourly. The reason is simple: users open the app maybe two or three times a day, so minute-level freshness adds almost nothing to the experience. Comparing .immediate and .hourly for a week each on a test device, the perceived battery cost was a non-trivial difference, and battery complaints in reviews are a churn driver that feeds straight into revenue.
As a rule of thumb, here is what I recommend. For a "look-back" app where users actively come to check a number, .hourly is plenty. For a "reminder" app that wants to notify goal completion instantly, raise the frequency but narrow the target to steps alone to contain battery impact. Because HealthKit wakes the app more often as you observe more types, my settled production stance is to always reason about type count and frequency together.
Your next move
Ship just one thing first — step reading, within the scope of Steps 1–3 above. Every additional HealthKit type raises your review burden, so shipping a minimal build, watching how users respond, then expanding to sleep and heart rate is the order that stays manageable for an indie developer. My own first release carried steps only, and I added sleep later as reactions came in. I hope it helps anyone trying to build a quieter experience on top of health data.
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.