How to Build a Heart Rate Monitor App in SwiftUI
A Heart Rate Monitor app reads live BPM data and tracks heart rate variability (HRV) over time using HealthKit — built for health-conscious users who want deeper insight than the default Apple Health UI provides.
Prerequisites
- Mac with Xcode 16+
- Apple Developer Program ($99/year) — required for TestFlight and App Store
- Basic Swift/SwiftUI knowledge
- A physical iPhone or Apple Watch — HealthKit heart rate data is not available in the Simulator
- Familiarity with async/await; HealthKit queries run on background threads
Architecture overview
The app uses a thin HealthKitManager service object that wraps HKHealthStore queries and publishes data upward via @Observable. SwiftData persists daily HRV summaries so Charts can render trends without re-querying HealthKit on every launch. The main view layer is a single DashboardView with child views for the live ring animation and the weekly chart.
HeartRateMonitorApp/ ├── App/ │ └── HeartRateMonitorApp.swift ├── Services/ │ └── HealthKitManager.swift ← HKHealthStore queries ├── Models/ │ └── HRVRecord.swift ← SwiftData @Model ├── Views/ │ ├── DashboardView.swift │ ├── LiveBeatView.swift ← animated ring │ └── HRVChartView.swift ← Swift Charts └── PrivacyInfo.xcprivacy
Step-by-step
1. Data model
Persist daily HRV summaries with SwiftData so the Charts view renders instantly without hitting HealthKit on every launch.
import SwiftData
import Foundation
@Model
final class HRVRecord {
var date: Date
var sdnnMilliseconds: Double // SDNN in ms — the standard HRV metric
var averageBPM: Double
var minBPM: Double
var maxBPM: Double
init(date: Date, sdnn: Double, avg: Double, min: Double, max: Double) {
self.date = date
self.sdnnMilliseconds = sdnn
self.averageBPM = avg
self.minBPM = min
self.maxBPM = max
}
/// Qualitative HRV tier based on SDNN ranges
var tier: String {
switch sdnnMilliseconds {
case 50...: return "High"
case 30..<50: return "Moderate"
default: return "Low"
}
}
}
2. Core UI — Dashboard & animated beat ring
The dashboard shows live BPM with a pulsing ring animation driven by a repeating withAnimation loop tied to the current heart rate.
struct LiveBeatView: View {
let bpm: Double
@State private var scale: CGFloat = 1.0
private var beatInterval: Double { 60.0 / max(bpm, 1) }
var body: some View {
ZStack {
Circle()
.stroke(Color.red.opacity(0.25), lineWidth: 6)
.frame(width: 160, height: 160)
Circle()
.stroke(Color.red, lineWidth: 4)
.frame(width: 160, height: 160)
.scaleEffect(scale)
.animation(
.easeOut(duration: beatInterval * 0.4)
.repeatForever(autoreverses: true),
value: scale
)
VStack(spacing: 2) {
Text("\(Int(bpm))")
.font(.system(size: 52, weight: .bold, design: .rounded))
.foregroundStyle(.red)
Text("BPM").font(.caption).foregroundStyle(.secondary)
}
}
.onAppear { scale = 1.12 }
}
}
3. HRV tracking via HealthKit
Query HKQuantityType(.heartRateVariabilitySDNN) for the past 30 days and expose the results as a published array your Charts view consumes directly.
import HealthKit
@Observable
final class HealthKitManager {
var latestBPM: Double = 0
var hrvRecords: [HKQuantitySample] = []
private let store = HKHealthStore()
func requestAuthorization() async throws {
let bpmType = HKQuantityType(.heartRate)
let hrvType = HKQuantityType(.heartRateVariabilitySDNN)
try await store.requestAuthorization(toShare: [], read: [bpmType, hrvType])
}
func fetchHRV() async {
let hrvType = HKQuantityType(.heartRateVariabilitySDNN)
let start = Calendar.current.date(byAdding: .day, value: -30, to: .now)!
let predicate = HKQuery.predicateForSamples(withStart: start, end: .now)
let descriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: hrvType, predicate: predicate)],
sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)]
)
if let results = try? await descriptor.result(for: store) {
await MainActor.run { self.hrvRecords = results }
}
}
}
4. Privacy Manifest (PrivacyInfo.xcprivacy)
Apple requires a Privacy Manifest declaring HealthKit usage and any required-reason API use; missing it causes automatic App Store rejection.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeHealth</string>
<key>NSPrivacyCollectedDataTypeLinked</key><false/>
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>CA92.1</string></array>
</dict>
</array>
</dict></plist>
Common pitfalls
- Testing on Simulator: HealthKit returns no live heart rate data in Simulator. Always test BPM queries and HRV reads on a physical device with an Apple Watch paired, or seed Health data manually via the Health app.
- Missing NSHealthShareUsageDescription: Forgetting to add the HealthKit usage string to
Info.plistcrashes the app on authorization. App Review will also reject if the string is too generic ("We use health data"). - Background delivery permissions: Enabling HKObserverQuery background delivery requires the HealthKit background modes entitlement. Apps requesting it without a clear user-facing reason frequently get rejected during App Review.
- HRV units mismatch:
HKQuantityType(.heartRateVariabilitySDNN)reports in seconds via HealthKit — divide by 1000 to display milliseconds to users, as SDNN is universally shown in ms. - App Store review — HealthKit entitlement scrutiny: Reviewers test that every requested HealthKit permission is demonstrably used in the UI. If you request write access but only read, expect a rejection requesting justification or removal of the write entitlement.
Adding monetization: Subscription
Use StoreKit 2's Product.products(for:) to load a monthly or annual auto-renewable subscription, then gate premium features — such as 90-day HRV trend history, CSV export, and custom alert thresholds — behind a Transaction.currentEntitlement(for:) check. Configure the subscription group in App Store Connect first, then use Product.SubscriptionInfo.Status to handle renewal, expiry, and billing-retry states gracefully so subscribers never see false paywalls. For a health app, annual subscriptions at a modest price point convert well because users associate HRV tracking with long-term wellness goals.
Shipping this faster with Soarias
Soarias scaffolds the full HealthKit-enabled Xcode project — including the PrivacyInfo.xcprivacy manifest, entitlements file with com.apple.developer.healthkit, and the StoreKit 2 subscription skeleton — in a single prompt. It also generates fastlane lanes for automated TestFlight uploads and pre-fills App Store Connect metadata (screenshots, privacy nutrition labels, health-data usage declarations) so you never stall on paperwork right before launch.
For an intermediate project like this one, most developers spend 1–2 days on HealthKit authorization edge cases and another half-day on the Privacy Manifest and ASC metadata. Soarias handles all of that scaffolding automatically, typically cutting the setup phase to under an hour and shaving 2–3 days off a one-week build cycle.
Related guides
FAQ
Do I need a paid Apple Developer account?
Yes. The free tier cannot enable HealthKit entitlements, install builds on devices via TestFlight, or submit to the App Store. The $99/year Apple Developer Program membership is required before you can run HealthKit queries on a physical device or distribute your app.
How do I submit this to the App Store?
Archive your app in Xcode (Product → Archive), upload via Xcode Organizer or xcrun altool, then complete the App Store Connect listing — including privacy nutrition labels for Health & Fitness data and a clear description of your HealthKit usage. Allow 1–3 days for App Review; HealthKit apps occasionally receive an extended review asking for a demo video of the health feature in action.
Can I show HRV data if the user doesn't have an Apple Watch?
Apple Watch is the primary source of HKQuantityType(.heartRateVariabilitySDNN) samples. Without a paired Watch, the HealthKit store will return no HRV samples — design your UI to handle this gracefully with an empty-state prompt explaining that HRV tracking requires Apple Watch, rather than showing a blank chart with no explanation.
Last reviewed: 2026-05-12 by the Soarias team.