How to Build a Blood Pressure Tracker App in SwiftUI

A Blood Pressure Tracker app lets users log systolic and diastolic readings over time and visualize trends with interactive charts. It's built for health-conscious users managing hypertension or monitoring cardiovascular wellness between doctor visits.

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

Prerequisites

Architecture overview

SwiftData is the on-device source of truth: a single BPReading model persists all readings, and @Query drives automatic UI updates without a separate ViewModel. HealthKit sits alongside SwiftData — readings are written as paired HKCorrelation objects so they surface correctly in the native Health app. The Charts framework consumes sorted SwiftData arrays directly via a dedicated BPTrendChart view. A lightweight HealthKitManager owns authorization and HK writes, keeping that side-effect logic out of views entirely.

BPTrackerApp/
├── Models/
│   └── BPReading.swift          # SwiftData @Model
├── Views/
│   ├── BPDashboardView.swift    # Main list + chart
│   ├── LogBPView.swift          # Input sheet
│   └── BPTrendChart.swift       # Charts view
├── Managers/
│   └── HealthKitManager.swift   # HK auth + writes
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define BPReading as a SwiftData @Model so readings persist automatically on-device — no CoreData stack, no manual saves.

import SwiftData
import Foundation

@Model
final class BPReading {
    var id: UUID = UUID()
    var systolic: Int
    var diastolic: Int
    var pulse: Int?
    var timestamp: Date = Date()

    init(systolic: Int, diastolic: Int, pulse: Int? = nil) {
        self.systolic = systolic
        self.diastolic = diastolic
        self.pulse = pulse
    }

    var category: String {
        switch systolic {
        case ..<120:    "Normal"
        case 120..<130: "Elevated"
        case 130..<140: "Stage 1 Hypertension"
        default:        "Stage 2 Hypertension"
        }
    }
}

2. Core UI — dashboard view

Use @Query with a reverse sort so the latest reading is always first, and pass the same array into the trend chart without additional state.

struct BPDashboardView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \BPReading.timestamp, order: .reverse) var readings: [BPReading]
    @State private var showLogger = false

    var body: some View {
        NavigationStack {
            List {
                BPTrendChart(readings: Array(readings.prefix(30)))
                    .frame(height: 180)
                    .listRowSeparator(.hidden)
                ForEach(readings) { BPReadingRow(reading: $0) }
                    .onDelete { offsets in
                        offsets.forEach { modelContext.delete(readings[$0]) }
                    }
            }
            .listStyle(.plain)
            .navigationTitle("Blood Pressure")
            .toolbar {
                Button { showLogger = true } label: {
                    Image(systemName: "plus.circle.fill")
                }
            }
            .sheet(isPresented: $showLogger) { LogBPView() }
        }
    }
}

3. BP logging with trend charts

Render systolic and diastolic as separate LineMark series and add a dashed RuleMark at 130 mmHg to visually flag Stage 1 hypertension.

import Charts

struct BPTrendChart: View {
    let readings: [BPReading]
    private var sorted: [BPReading] {
        readings.sorted { $0.timestamp < $1.timestamp }
    }

    var body: some View {
        Chart {
            ForEach(sorted) { r in
                LineMark(x: .value("Date", r.timestamp, unit: .day),
                         y: .value("Systolic", r.systolic))
                    .foregroundStyle(.red.gradient)
                LineMark(x: .value("Date", r.timestamp, unit: .day),
                         y: .value("Diastolic", r.diastolic))
                    .foregroundStyle(.blue.gradient)
            }
            RuleMark(y: .value("Stage 1", 130))
                .lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
                .foregroundStyle(.orange.opacity(0.7))
        }
        .chartYScale(domain: 50...200)
        .chartLegend(position: .bottom)
    }
}

4. Privacy Manifest setup

HealthKit requires both a Privacy Manifest and two Info.plist usage strings — the App Store will reject your binary without them.

// 1. Add PrivacyInfo.xcprivacy (File → New → Resource → App Privacy)
//    NSPrivacyAccessedAPICategoryUserDefaults → CA92.1

// 2. Info.plist — required HealthKit usage strings:
//    NSHealthShareUsageDescription  → "Read past BP readings from Health."
//    NSHealthUpdateUsageDescription → "Save BP readings to Apple Health."

// 3. Entitlements: com.apple.developer.healthkit = YES

// 4. Request authorization at launch (real device only):
import HealthKit

func requestHealthKitAccess() async {
    guard HKHealthStore.isHealthDataAvailable() else { return }
    let store = HKHealthStore()
    let bpTypes: Set<HKSampleType> = [
        HKQuantityType(.bloodPressureSystolic),
        HKQuantityType(.bloodPressureDiastolic)
    ]
    try? await store.requestAuthorization(toShare: bpTypes, read: bpTypes)
}

Common pitfalls

Adding monetization: Subscription

Use StoreKit 2's Product API to offer a monthly or annual subscription that unlocks unlimited history exports, PDF reports, and trend analysis beyond 30 days. Define your subscription group in App Store Connect under Monetization → Subscriptions, then call Product.products(for:) at startup to fetch current pricing. Present the paywall with SubscriptionStoreView (iOS 17+) or a custom sheet — both gate features via Transaction.currentEntitlements. Wire up a Task { for await _ in Transaction.updates { … } } listener on app launch so reinstalls and family sharing restore automatically without user action.

Shipping this faster with Soarias

Soarias scaffolds the SwiftData model, wires the HealthKit entitlement and both Info.plist usage strings, drops a pre-filled PrivacyInfo.xcprivacy with the correct NSPrivacyAccessedAPITypes keys into your target, stubs out StoreKit subscription product IDs linked to your App Store Connect record, and configures fastlane lanes for TestFlight upload and App Store submission — all before you write a line of feature code.

For an intermediate project like a Blood Pressure Tracker, developers typically spend 3–5 hours on infrastructure: HealthKit entitlements, Privacy Manifest, Matchfile/Appfile setup, and screenshot automation. Soarias compresses that to under 15 minutes, leaving your full week to focus on the Charts trend view, the logging sheet UX, and the subscription paywall.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. A free account lets you sideload to a personal device, but TestFlight distribution and App Store submission both require the $99/year Apple Developer Program membership. Because HealthKit only works on a real device, you'll need an active developer account from the first day of testing — not just at submission time.

How do I submit this to the App Store?

Archive your app in Xcode (Product → Archive), then use the Organizer to upload to App Store Connect. In ASC, complete your screenshots, privacy nutrition labels, and age rating, then submit for review. Expect 1–3 days for an initial decision. Health category apps sometimes receive additional scrutiny — ensure your metadata clearly describes personal tracking, not clinical diagnosis.

Does my Blood Pressure Tracker need FDA clearance?

A logging app that records readings manually entered by the user is generally considered a general wellness app and does not require FDA clearance. However, if your app algorithmically analyzes readings to provide a medical recommendation or automated emergency alert, it may fall under FDA's Software as a Medical Device framework. Keep your feature set focused on logging and visualization, and consult the FDA's Digital Health Center of Excellence resources if you plan to add any automated clinical guidance.

Last reviewed: 2026-05-12 by the Soarias team.