```html How to Build a Heart Rate Monitor App in SwiftUI (2026)

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.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Intermediate Estimated time: 1 week Updated: May 12, 2026

Prerequisites

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

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.

```