Build a Fitness Tracker: HealthKit Integration and Swift Charts
Violet logs her morning runs. Dash tracks his sprints — every blazing one of them. Mr. Incredible monitors his bench press sets and keeps a suspicious eye on his step count. They all want one app to show it all, beautifully charted. That app is what you’re about to build.
In this tutorial, you’ll build The Incredibles Family Fitness Tracker — a SwiftUI dashboard that reads real health
data from HealthKit, charts weekly step trends and heart rate
history using Swift Charts, and displays animated ring-style
progress indicators. You’ll learn how to request HealthKit authorization, wrap asynchronous queries in a clean data
layer, and compose rich data visualizations with BarMark, LineMark, and SectorMark.
Prerequisites
- Xcode 16+ with an iOS 18 deployment target
- A physical iPhone or a simulator with Health data seeded (see Getting Started)
- Familiarity with SwiftUI state management
- Familiarity with async/await
Note: HealthKit is available on iPhone and Apple Watch only. It is not available on iPad or macOS (without Mac Catalyst workarounds). The simulator supports HealthKit queries but you must manually add sample data via the Health app simulation.
Contents
- Getting Started
- Step 1: Adding the HealthKit Capability
- Step 2: Requesting HealthKit Authorization
- Step 3: Building the HealthKit Manager
- Step 4: Building the Daily Steps Ring View
- Step 5: Charting Weekly Steps with Swift Charts
- Step 6: Adding a Heart Rate Line Chart
- Step 7: Monthly Activity Breakdown with SectorMark
- Step 8: Querying the Workout Log
- Where to Go From Here?
Getting Started
Open Xcode and create a new project using the App template.
- Set the Product Name to
IncrediblesFitness. - Set Interface to SwiftUI and Language to Swift.
- Set Minimum Deployments to iOS 18.0.
- Choose a location to save the project and click Finish.
The finished app will have this file structure:
IncrediblesFitness/
├── IncrediblesFitnessApp.swift
├── ContentView.swift
├── Managers/
│ └── HealthKitManager.swift
├── Models/
│ └── FitnessModels.swift
├── Views/
│ ├── DashboardView.swift
│ ├── StepsRingView.swift
│ ├── WeeklyChartView.swift
│ ├── HeartRateChartView.swift
│ ├── MonthlyBreakdownView.swift
│ └── WorkoutLogView.swift
Create those folders and empty Swift files now so Xcode’s navigator stays organized. You can leave each file with just
import SwiftUI until the relevant step.
Seeding simulator data: If you’re running on a simulator, open the Health app inside the simulator, navigate to Browse → Activity → Steps, and use the Add Data button to add a few weeks of step counts. Do the same for Heart Rate and Active Energy. This ensures your charts have something to render.
Step 1: Adding the HealthKit Capability
Before writing a single line of HealthKit code, you need to add the capability and the required privacy keys — without them, the app will crash at launch.
Adding the entitlement:
- Select the
IncrediblesFitnesstarget in the project navigator. - Click the Signing & Capabilities tab.
- Click the + button and add HealthKit.
Xcode automatically creates IncrediblesFitness.entitlements with com.apple.developer.healthkit set to true.
Adding the Info.plist privacy key:
HealthKit requires you to explain to the user why you need access to their health data. Open Info.plist (or add the
key via Info tab of your target) and add:
| Key | Value |
|---|---|
NSHealthShareUsageDescription | The Incredibles Fitness Tracker reads your steps, heart rate, and workouts to display your family's fitness progress. |
If you intend to write data back to HealthKit (which this tutorial does not cover), you’d also add
NSHealthUpdateUsageDescription.
Warning: Apple will reject your App Store submission if
NSHealthShareUsageDescriptionis missing. Even in development, missing this key causes a crash with a descriptive error in the console.
Step 2: Requesting HealthKit Authorization
HKHealthStore is the central object for all
HealthKit interactions. You create one instance per app — it’s expensive to instantiate and acts as a gateway for both
authorization and queries.
Create a new file at Managers/HealthKitManager.swift. This class will own the HKHealthStore, handle authorization,
and expose async methods for fetching data.
Start with the authorization request:
import HealthKit
@Observable
final class HealthKitManager {
// One shared store for the lifetime of the app
private let store = HKHealthStore()
// The health data types we want to read
private let readTypes: Set<HKObjectType> = [
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.workoutType()
]
var isAuthorized = false
var authorizationError: Error?
func requestAuthorization() async {
// HealthKit is not available on all devices (e.g., iPad without workaround)
guard HKHealthStore.isHealthDataAvailable() else {
return
}
do {
try await store.requestAuthorization(toShare: [], read: readTypes)
isAuthorized = true
} catch {
authorizationError = error
}
}
}
A few things worth noting here. The readTypes set uses force-unwrap because these identifiers are known at compile
time and will never be nil — this is one of the rare cases where ! is justified. We’re passing an empty Set as
toShare because this app only reads data, never writes it. The @Observable macro means SwiftUI views will
automatically re-render when isAuthorized or authorizationError change.
Now wire up the authorization request from the app’s entry point. Open IncrediblesFitnessApp.swift:
import SwiftUI
@main
struct IncrediblesFitnessApp: App {
@State private var healthKitManager = HealthKitManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(healthKitManager)
.task {
await healthKitManager.requestAuthorization()
}
}
}
}
Using .task here means the authorization request fires as soon as the view appears, without blocking the main thread.
Checkpoint: Build and run the app on a device or simulator. You should see a HealthKit permission sheet slide up asking for access to Steps, Active Energy, Heart Rate, and Workouts. Tap Allow All to grant access. If the sheet never appears, double-check that
NSHealthShareUsageDescriptionis in yourInfo.plistand the HealthKit entitlement is enabled.
Step 3: Building the HealthKit Manager
With authorization in place, you can now query actual health data. HealthKit offers several query types; this tutorial uses two:
HKStatisticsCollectionQuery— aggregates samples over a date range into daily/weekly buckets. Perfect for step counts and active energy.HKSampleQuery— fetches individual samples. Used here for heart rate readings.
Add these methods to HealthKitManager. First, define the data models that the manager will vend. Open
Models/FitnessModels.swift:
import Foundation
struct DailySteps: Identifiable {
let id = UUID()
let date: Date
let count: Double
}
struct HeartRateSample: Identifiable {
let id = UUID()
let date: Date
let beatsPerMinute: Double
}
struct WorkoutRecord: Identifiable {
let id = UUID()
let date: Date
let activityType: String
let durationMinutes: Double
let caloriesBurned: Double
let distanceKilometers: Double?
}
Now add the query methods back in HealthKitManager.swift. Add these stored properties near the top:
var todaySteps: Double = 0
var weeklySteps: [DailySteps] = []
var heartRateSamples: [HeartRateSample] = []
var workouts: [WorkoutRecord] = []
var activeEnergyToday: Double = 0
Then add the step count query. HKStatisticsCollectionQuery is callback-based, so we bridge it to async/await using
withCheckedContinuation:
func fetchWeeklySteps() async {
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return }
let calendar = Calendar.current
let now = Date()
// Start of 7 days ago
guard let startDate = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: now)) else { return }
let anchorDate = calendar.startOfDay(for: now)
let daily = DateComponents(day: 1)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: now, options: .strictStartDate)
let samples: [DailySteps] = await withCheckedContinuation { continuation in
let query = HKStatisticsCollectionQuery(
quantityType: stepType,
quantitySamplePredicate: predicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: daily
)
query.initialResultsHandler = { _, results, _ in
var dailyData: [DailySteps] = []
results?.enumerateStatistics(from: startDate, to: now) { stats, _ in
let steps = stats.sumQuantity()?.doubleValue(for: .count()) ?? 0
dailyData.append(DailySteps(date: stats.startDate, count: steps))
}
continuation.resume(returning: dailyData)
}
store.execute(query)
}
weeklySteps = samples
todaySteps = samples.last?.count ?? 0
}
Now add the heart rate query. Heart rate is stored as individual samples in beats per minute, so HKSampleQuery is the
right tool:
func fetchHeartRate(days: Int = 7) async {
guard let hrType = HKQuantityType.quantityType(forIdentifier: .heartRate) else { return }
let now = Date()
guard let startDate = Calendar.current.date(byAdding: .day, value: -days, to: now) else { return }
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: now, options: .strictStartDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)
let samples: [HeartRateSample] = await withCheckedContinuation { continuation in
let query = HKSampleQuery(
sampleType: hrType,
predicate: predicate,
limit: 200, // cap at 200 readings to keep the chart readable
sortDescriptors: [sortDescriptor]
) { _, results, _ in
let hrSamples = (results as? [HKQuantitySample])?.map { sample in
HeartRateSample(
date: sample.endDate,
// HealthKit stores heart rate as count/min
beatsPerMinute: sample.quantity.doubleValue(for: HKUnit(from: "count/min"))
)
} ?? []
continuation.resume(returning: hrSamples)
}
store.execute(query)
}
heartRateSamples = samples
}
Add one more method to fetch today’s active energy:
func fetchActiveEnergyToday() async {
guard let energyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else { return }
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
let calories: Double = await withCheckedContinuation { continuation in
let query = HKStatisticsQuery(
quantityType: energyType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { _, result, _ in
let value = result?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
continuation.resume(returning: value)
}
store.execute(query)
}
activeEnergyToday = calories
}
/// Convenience method to refresh all data at once
func fetchAll() async {
await withTaskGroup(of: Void.self) { group in
group.addTask { await self.fetchWeeklySteps() }
group.addTask { await self.fetchHeartRate() }
group.addTask { await self.fetchActiveEnergyToday() }
group.addTask { await self.fetchWorkouts() }
}
}
The fetchAll() method uses a TaskGroup so all four queries run concurrently — HealthKit handles them in parallel,
and the UI gets updated as each one completes. You’ll add fetchWorkouts() in Step 8.
Step 4: Building the Daily Steps Ring View
The centerpiece of the dashboard is a progress ring showing today’s steps as a fraction of the 10,000-step goal — just
the kind of display Mr. Incredible would demand for his training regime. This ring uses SwiftUI’s Circle with the
.trim(from:to:) modifier, which clips the stroke to a fraction of the circumference.
Create Views/StepsRingView.swift:
import SwiftUI
struct StepsRingView: View {
let steps: Double
let goal: Double = 10_000
private var progress: Double {
min(steps / goal, 1.0) // clamp at 100%
}
var body: some View {
ZStack {
// Background track
Circle()
.stroke(Color(.systemGray5), lineWidth: 18)
// Progress arc — trimmed from the top (rotated -90°)
Circle()
.trim(from: 0, to: progress)
.stroke(
AngularGradient(
gradient: Gradient(colors: [.red, .orange]),
center: .center
),
style: StrokeStyle(lineWidth: 18, lineCap: .round)
)
.rotationEffect(.degrees(-90))
.animation(.easeOut(duration: 1.0), value: progress)
// Center label
VStack(spacing: 4) {
Text(steps, format: .number)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
Text("of \(Int(goal)) steps")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(width: 160, height: 160)
}
}
The .trim(from: 0, to: progress) modifier clips the circle’s stroke. Because Circle starts drawing at the 3 o’clock
position by default, we apply .rotationEffect(.degrees(-90)) to start at 12 o’clock instead — which matches the
familiar Activity ring appearance on Apple Watch.
Now set up the main dashboard. Open Views/DashboardView.swift:
import SwiftUI
struct DashboardView: View {
@Environment(HealthKitManager.self) private var hkManager
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
// Today's stats header
VStack(spacing: 8) {
Text("Today's Activity")
.font(.title2.bold())
Text("The Incredible Family Tracker")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.top)
// Steps ring
StepsRingView(steps: hkManager.todaySteps)
// Active energy callout
HStack {
Image(systemName: "flame.fill")
.foregroundStyle(.orange)
Text("\(Int(hkManager.activeEnergyToday)) kcal active energy")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Divider()
// Charts placeholder (built in steps 5 and 6)
WeeklyChartView()
HeartRateChartView()
MonthlyBreakdownView()
WorkoutLogView()
}
.padding(.horizontal)
}
.navigationTitle("Incredible Fitness")
.task {
await hkManager.fetchAll()
}
.refreshable {
await hkManager.fetchAll()
}
}
}
}
Update ContentView.swift to show the dashboard:
import SwiftUI
struct ContentView: View {
var body: some View {
DashboardView()
}
}
Checkpoint: Build and run. The dashboard should load with the steps ring in the center displaying today’s step count (or 0 if the simulator has no data). The progress arc animates in from 0 to the current fraction when the view appears. Pull to refresh re-queries all health data.
Step 5: Charting Weekly Steps with Swift Charts
Swift Charts is a declarative charting framework introduced at
WWDC 2022. You describe your chart using mark types — BarMark, LineMark, PointMark, etc. — and the framework
handles rendering, axes, legends, and accessibility automatically.
Create Views/WeeklyChartView.swift. This view will render a bar chart showing the past seven days of step counts, with
a horizontal rule at the 10,000-step goal line:
import SwiftUI
import Charts
struct WeeklyChartView: View {
@Environment(HealthKitManager.self) private var hkManager
private let goal: Double = 10_000
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Weekly Steps")
.font(.headline)
Chart {
// Goal line — drawn before the bars so it renders behind them
RuleMark(y: .value("Goal", goal))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [4]))
.foregroundStyle(.secondary)
.annotation(position: .top, alignment: .leading) {
Text("Goal")
.font(.caption2)
.foregroundStyle(.secondary)
}
ForEach(hkManager.weeklySteps) { day in
BarMark(
x: .value("Day", day.date, unit: .day),
y: .value("Steps", day.count)
)
.foregroundStyle(day.count >= goal ? Color.green : Color.red)
.cornerRadius(4)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
.frame(height: 200)
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
There are a few chart-specific patterns to understand here. The
Chart view is the container — everything inside its closure
describes the marks to render. RuleMark(y:) draws a horizontal line at a constant y-value, which we use for the goal
threshold. BarMark(x:y:) plots the bars; we use .value("Day", day.date, unit: .day) so the x-axis automatically
groups by calendar day. The conditional .foregroundStyle colors bars green if the goal was met and red otherwise —
Violet hit her goal, Dash always does, Mr. Incredible had a lazy Sunday.
Tip: Swift Charts automatically infers axis scales from the data range. If you want to force the y-axis to always start at 0 (rather than the minimum value in the dataset), add
.chartYScale(domain: 0...maxValue)to theChartview.
Step 6: Adding a Heart Rate Line Chart
Heart rate data tells a more nuanced story than step counts — it reveals workout intensity and resting recovery. A
LineMark with PointMark overlaid is the right visualization here: the line shows the trend, and individual dots
reveal the density of readings.
Create Views/HeartRateChartView.swift:
import SwiftUI
import Charts
struct HeartRateChartView: View {
@Environment(HealthKitManager.self) private var hkManager
// Compute average for a reference line
private var averageBPM: Double {
guard !hkManager.heartRateSamples.isEmpty else { return 0 }
let total = hkManager.heartRateSamples.reduce(0) { $0 + $1.beatsPerMinute }
return total / Double(hkManager.heartRateSamples.count)
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Heart Rate (7 days)")
.font(.headline)
Spacer()
if averageBPM > 0 {
Label("\(Int(averageBPM)) avg bpm", systemImage: "heart.fill")
.font(.caption)
.foregroundStyle(.red)
}
}
if hkManager.heartRateSamples.isEmpty {
Text("No heart rate data available.\nAdd data in the Health app.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.frame(height: 180)
} else {
Chart {
// Average reference line
if averageBPM > 0 {
RuleMark(y: .value("Average", averageBPM))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [4]))
.foregroundStyle(.red.opacity(0.5))
}
// Line connecting samples
ForEach(hkManager.heartRateSamples) { sample in
LineMark(
x: .value("Time", sample.date),
y: .value("BPM", sample.beatsPerMinute)
)
.foregroundStyle(.red)
.interpolationMethod(.catmullRom) // smooth curve
}
// Individual data points
ForEach(hkManager.heartRateSamples) { sample in
PointMark(
x: .value("Time", sample.date),
y: .value("BPM", sample.beatsPerMinute)
)
.foregroundStyle(.red)
.symbolSize(20)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.abbreviated).day())
}
}
.chartYScale(domain: 40...200) // typical resting-to-max range
.frame(height: 180)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
The .interpolationMethod(.catmullRom) call smooths the line between data points using a Catmull-Rom spline. This makes
the chart read more like a continuous trend rather than a jagged series of connected dots — appropriate for heart rate
data where you expect smooth biological variation.
Checkpoint: Build and run. The Weekly Steps chart should show colored bars for the past seven days, and the Heart Rate chart should show a smooth red line. If you’re on the simulator with manually added data, both charts should be populated. If you’re on a physical device with Health data, you’ll see your real readings.
Step 7: Monthly Activity Breakdown with SectorMark
For a high-level overview of how the family splits its activity across types — steps, active energy, and dedicated
workouts — a pie chart communicates proportions at a glance. Swift Charts provides SectorMark for this.
Create Views/MonthlyBreakdownView.swift. This view computes percentage breakdowns from the data already fetched:
import SwiftUI
import Charts
struct ActivitySlice: Identifiable {
let id = UUID()
let label: String
let value: Double
let color: Color
}
struct MonthlyBreakdownView: View {
@Environment(HealthKitManager.self) private var hkManager
private var slices: [ActivitySlice] {
let totalSteps = hkManager.weeklySteps.reduce(0) { $0 + $1.count }
let totalCalories = hkManager.activeEnergyToday // simplified: today only
let workoutMinutes = hkManager.workouts.reduce(0) { $0 + $1.durationMinutes }
// Normalize to a common unit so the chart is meaningful
// We treat 1000 steps ≈ 1 unit, 100 kcal ≈ 1 unit, 10 min workout ≈ 1 unit
let stepsUnit = totalSteps / 1000
let calsUnit = totalCalories / 100
let workoutUnit = workoutMinutes / 10
let total = stepsUnit + calsUnit + workoutUnit
guard total > 0 else { return [] }
return [
ActivitySlice(label: "Steps", value: stepsUnit / total, color: .blue),
ActivitySlice(label: "Active Energy", value: calsUnit / total, color: .orange),
ActivitySlice(label: "Workouts", value: workoutUnit / total, color: .green)
]
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Activity Mix")
.font(.headline)
if slices.isEmpty {
Text("No activity data yet.")
.font(.subheadline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.frame(height: 200)
} else {
HStack(alignment: .center, spacing: 20) {
Chart(slices) { slice in
SectorMark(
angle: .value("Share", slice.value),
innerRadius: .ratio(0.55), // donut hole
angularInset: 2 // gap between sectors
)
.foregroundStyle(slice.color)
.cornerRadius(4)
}
.frame(width: 150, height: 150)
// Legend
VStack(alignment: .leading, spacing: 8) {
ForEach(slices) { slice in
HStack(spacing: 8) {
RoundedRectangle(cornerRadius: 3)
.fill(slice.color)
.frame(width: 12, height: 12)
Text(slice.label)
.font(.caption)
Spacer()
Text("\(Int(slice.value * 100))%")
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
}
}
}
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
SectorMark takes an angle parameter that represents how large each sector should be — Swift Charts computes the
actual angles from the proportional values automatically. The innerRadius: .ratio(0.55) parameter turns it into a
donut chart, which is generally more readable than a fully filled pie when you have a legend alongside it. The
angularInset: 2 adds a small visual gap between slices.
Tip:
SectorMarkwas introduced in iOS 17. Since our deployment target is iOS 18, we can use it freely. If you needed to support iOS 16, you’d implement the pie chart manually usingPath— see the SwiftUI Custom Shapes post for that approach.
Step 8: Querying the Workout Log
The final piece is a list of recorded workouts — Mrs. Incredible’s yoga sessions, Dash’s track workouts, Mr.
Incredible’s strength training. HealthKit stores workouts as
HKWorkout objects, which carry duration, calorie
count, distance, and a workout type enum.
Add fetchWorkouts() to HealthKitManager.swift:
func fetchWorkouts(limit: Int = 20) async {
let workoutType = HKObjectType.workoutType()
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let predicate = HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .month, value: -1, to: Date()),
end: Date(),
options: .strictStartDate
)
let records: [WorkoutRecord] = await withCheckedContinuation { continuation in
let query = HKSampleQuery(
sampleType: workoutType,
predicate: predicate,
limit: limit,
sortDescriptors: [sortDescriptor]
) { _, results, _ in
let workoutSamples = (results as? [HKWorkout]) ?? []
let mapped: [WorkoutRecord] = workoutSamples.map { workout in
let durationMins = workout.duration / 60
let calories = workout.statistics(for: HKQuantityType(.activeEnergyBurned))?
.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
let distanceKm = workout.statistics(for: HKQuantityType(.distanceWalkingRunning))?
.sumQuantity()?.doubleValue(for: .meterUnit(with: .kilo))
return WorkoutRecord(
date: workout.startDate,
activityType: workout.workoutActivityType.name,
durationMinutes: durationMins,
caloriesBurned: calories,
distanceKilometers: distanceKm
)
}
continuation.resume(returning: mapped)
}
store.execute(query)
}
workouts = records
}
HKWorkout stores associated statistics — calories, distance — via statistics(for:). Note that distance is only
present for activity types that track movement (running, cycling, walking); strength training workouts won’t have a
distance value, which is why distanceKilometers is optional.
The workout.workoutActivityType.name property doesn’t exist on HKWorkoutActivityType by default. Add this extension
at the bottom of FitnessModels.swift to give each type a human-readable name:
import HealthKit
extension HKWorkoutActivityType {
var name: String {
switch self {
case .running: return "Running"
case .cycling: return "Cycling"
case .swimming: return "Swimming"
case .walking: return "Walking"
case .functionalStrengthTraining: return "Strength Training"
case .yoga: return "Yoga"
case .highIntensityIntervalTraining: return "HIIT"
default: return "Workout"
}
}
}
Create Views/WorkoutLogView.swift to display the list:
import SwiftUI
struct WorkoutLogView: View {
@Environment(HealthKitManager.self) private var hkManager
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Recent Workouts")
.font(.headline)
if hkManager.workouts.isEmpty {
Text("No workouts in the past month.\nTime to suit up!")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(.vertical, 20)
} else {
ForEach(hkManager.workouts) { workout in
WorkoutRowView(workout: workout)
Divider()
}
}
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
private struct WorkoutRowView: View {
let workout: WorkoutRecord
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(workout.activityType)
.font(.subheadline.bold())
Text(workout.date, format: .dateTime.month().day().hour().minute())
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
Text("\(Int(workout.durationMinutes)) min")
.font(.subheadline.monospacedDigit())
HStack(spacing: 4) {
Text("\(Int(workout.caloriesBurned)) kcal")
if let dist = workout.distanceKilometers {
Text("· \(dist, specifier: "%.1f") km")
}
}
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
}
Checkpoint: Build and run the complete app. You should see all four sections of the dashboard: the steps ring at the top, the weekly bar chart, the heart rate line chart, the activity mix donut chart, and the workout log at the bottom. Pull down to refresh all data. If you’re on a physical device with recorded workouts, they’ll appear in the log. On the simulator, add workout data through the Health app’s built-in simulator support or seed it programmatically via
HKWorkoutBuilder.
Where to Go From Here?
Congratulations! You’ve built The Incredibles Family Fitness Tracker — a HealthKit-powered SwiftUI dashboard with four distinct visualizations, async data fetching, and a clean manager architecture.
Here’s what you learned:
- How to add the HealthKit entitlement and privacy keys required by App Store review
- How to request health data authorization using
HKHealthStore.requestAuthorization(toShare:read:) - How to bridge HealthKit’s callback-based
HKStatisticsCollectionQueryandHKSampleQuerytoasync/awaitusingwithCheckedContinuation - How to compose
BarMark,LineMark,PointMark, andSectorMarkin Swift Charts to tell different stories with the same data - How to build an animated progress ring using
Circle().trim(from:to:)withAngularGradient - How to query
HKWorkoutrecords and extract per-workout statistics for calories and distance
Ideas for extending this project:
- Apple Watch complication — use
WidgetKitandCLKComplicationDataSourceto push the steps ring to the watch face so Mr. Incredible can check his progress without reaching for his phone. - Background delivery — call
HKHealthStore.enableBackgroundDelivery(for:frequency:withCompletion:)so the app updates its data even when it’s not in the foreground. This is how the Activity app keeps rings in sync. - Workout recording — use
HKWorkoutBuilderto let users start, pause, and stop workout sessions directly from the app, writing data back to HealthKit. - Family sharing — explore
HKDocumentQueryand SharePlay to let the Parr family compare their stats in a shared session. - Trend analysis — use
HKStatisticsQuerywith.discreteAverageto compute resting heart rate trends over 30 or 90 days, then highlight improvements with conditional chart annotations.