```html How to Build a Step Counter App in SwiftUI (2026)

How to Build a Step Counter App in SwiftUI

A step counter app reads daily step data from HealthKit, visualises progress toward a configurable goal, and surfaces a home-screen widget so users never have to open the app to stay motivated. It's an ideal first health app for developers new to the Apple ecosystem.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Beginner Estimated time: 1–2 weekends Updated: May 12, 2026

Prerequisites

Architecture overview

The app uses a thin HealthKitManager observable class as its data layer, which requests permissions, queries HKStatisticsCollectionQuery for the last 7 days of step data, and enables background delivery. SwiftData caches the most recent step totals so the WidgetKit extension can read them from an App Group container without hitting HealthKit itself. A single StepDashboardView owns the navigation stack; Swift Charts renders the weekly bar chart inline.

StepCounterApp/
├── App/
│   └── StepCounterApp.swift
├── Health/
│   └── HealthKitManager.swift
├── Models/
│   └── DailySteps.swift          ← SwiftData @Model
├── Views/
│   ├── StepDashboardView.swift
│   └── WeeklyChartView.swift
├── Widget/
│   └── StepWidget.swift
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define a SwiftData @Model that caches each day's step count so the widget can read it from a shared App Group without re-querying HealthKit.

import SwiftData
import Foundation

@Model
final class DailySteps {
    @Attribute(.unique) var date: Date
    var count: Int
    var goalMet: Bool

    init(date: Date, count: Int, goal: Int = 10_000) {
        self.date = Calendar.current.startOfDay(for: date)
        self.count = count
        self.goalMet = count >= goal
    }
}

// In your App entry point:
// .modelContainer(for: DailySteps.self,
//     inMemory: false,
//     isAutosaveEnabled: true,
//     isUndoEnabled: false,
//     onSetup: { _ in })

2. Core UI — step dashboard

Build the main view with a circular progress ring for today's steps and a Swift Charts weekly bar chart below it.

import SwiftUI
import Charts

struct StepDashboardView: View {
    @State private var hkManager = HealthKitManager()
    let goal = 10_000

    var progress: Double {
        min(Double(hkManager.todaySteps) / Double(goal), 1.0)
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 28) {
                ZStack {
                    Circle()
                        .stroke(Color.secondary.opacity(0.2), lineWidth: 18)
                    Circle()
                        .trim(from: 0, to: progress)
                        .stroke(Color.green, style: StrokeStyle(lineWidth: 18, lineCap: .round))
                        .rotationEffect(.degrees(-90))
                        .animation(.easeOut, value: progress)
                    VStack(spacing: 4) {
                        Text("\(hkManager.todaySteps)")
                            .font(.system(size: 44, weight: .bold, design: .rounded))
                        Text("of \(goal) steps")
                            .font(.caption).foregroundStyle(.secondary)
                    }
                }
                .frame(width: 220, height: 220)
                .padding(.top, 32)

                WeeklyChartView(data: hkManager.weeklySteps, goal: goal)
                    .frame(height: 180)
                    .padding(.horizontal)
            }
        }
        .navigationTitle("Steps")
        .task { await hkManager.requestAuthorization() }
    }
}

3. Daily step tracking with HealthKit

Query HealthKit for today's steps and enable background delivery so counts update silently when the app is in the background.

import HealthKit
import Observation

@Observable
final class HealthKitManager {
    var todaySteps: Int = 0
    var weeklySteps: [(date: Date, steps: Int)] = []

    private let store = HKHealthStore()
    private let stepType = HKQuantityType(.stepCount)

    func requestAuthorization() async {
        guard HKHealthStore.isHealthDataAvailable() else { return }
        try? await store.requestAuthorization(toShare: [], read: [stepType])
        await fetchWeeklySteps()
        enableBackgroundDelivery()
    }

    func fetchWeeklySteps() async {
        let start = Calendar.current.date(byAdding: .day, value: -6, to: .now)!
        let anchor = Calendar.current.startOfDay(for: start)
        let interval = DateComponents(day: 1)
        let predicate = HKQuery.predicateForSamples(withStart: anchor, end: .now)

        let results = try? await withCheckedThrowingContinuation { cont in
            let q = HKStatisticsCollectionQuery(
                quantityType: stepType,
                quantitySamplePredicate: predicate,
                options: .cumulativeSum,
                anchorDate: anchor,
                intervalComponents: interval)
            q.initialResultsHandler = { _, col, err in
                if let err { cont.resume(throwing: err); return }
                cont.resume(returning: col)
            }
            store.execute(q)
        }

        var entries: [(Date, Int)] = []
        results?.enumerateStatistics(from: anchor, to: .now) { stat, _ in
            let count = Int(stat.sumQuantity()?.doubleValue(for: .count()) ?? 0)
            entries.append((stat.startDate, count))
        }
        weeklySteps = entries.map { (date: $0.0, steps: $0.1) }
        todaySteps = entries.last?.1 ?? 0
    }

    private func enableBackgroundDelivery() {
        store.enableBackgroundDelivery(for: stepType, frequency: .hourly) { _, _ in }
    }
}

4. Privacy Manifest (PrivacyInfo.xcprivacy)

Apple requires a Privacy Manifest for any app using HealthKit or certain system APIs — missing it is a common cause of App Store rejections in 2025–26.

<?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>NSPrivacyTracking</key><false/>
  <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: Ad-supported

The most pragmatic ad integration for a beginner health app is Google AdMob's banner or interstitial format, displayed on the weekly summary screen rather than the active tracking view (Apple's HIG discourages ads near health data). Add the AdMob SDK via Swift Package Manager, initialise GADMobileAds.sharedInstance().start() in your App init, then wrap a GADBannerView in a UIViewRepresentable. Keep the ad unit IDs in a .xcconfig file so they're not hard-coded — App Store review has flagged hard-coded test IDs submitted in production builds.

Shipping this faster with Soarias

Soarias scaffolds the HealthKit permission flow, App Group entitlements, and WidgetKit target in under a minute — the three setup steps that typically consume most of a first weekend. It also generates the PrivacyInfo.xcprivacy with the correct API-access reason codes pre-filled, configures fastlane match for code signing, and drives the App Store Connect submission including screenshot upload and metadata.

For a beginner-complexity app like this one, most developers report saving four to six hours compared to doing the Xcode target wiring, entitlement setup, and ASC form-filling by hand. That's roughly one full weekend reclaimed — enough to ship a second app in the same sprint.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The free tier lets you sideload to your own device via Xcode, but you need the $99/year Apple Developer Program membership to distribute on TestFlight or submit to the App Store. HealthKit apps also require the membership to enable the entitlement in App Store Connect.

How do I submit this to the App Store?

Archive your app in Xcode (Product → Archive), validate and upload via Xcode Organizer or xcrun altool, then complete the App Store Connect listing — screenshots, privacy nutrition labels, HealthKit usage description, and your Privacy Manifest. Apple typically reviews health apps within 24–48 hours. Soarias automates all of this from the command line.

Can my step counter widget update in real time?

WidgetKit timelines refresh on Apple's schedule — typically every 15–30 minutes for most widget slots. You can call WidgetCenter.shared.reloadTimelines(ofKind:) from your app when HealthKit background delivery fires to push a fresh count sooner, but you cannot guarantee sub-minute latency on the home screen without using a Live Activity (which requires the ActivityKit framework and a persistent foreground or Always On display).

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

```