How to Build a Quit Smoking App in SwiftUI

A Quit Smoking app tracks how long someone has been smoke-free, how much money they have saved, and how many cigarettes they have avoided — giving real-time motivation through visible numbers. It is ideal for anyone quitting cold-turkey or tapering, and for developers looking for a straightforward Health & Fitness debut on the App Store.

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

Prerequisites

Architecture overview

The data layer is a single SwiftData @Model (QuitSession) stored entirely on-device — no backend, no network calls. DashboardView owns a @Query that fetches the active session and pipes computed stats into reusable StatCard components. A Swift Charts AreaMark chart in SavingsChartView renders cumulative savings per day. UserNotifications fires milestone alerts at 1 day, 1 week, and 1 month using a simple local trigger registered on quit-date entry. State flows down via the shared ModelContainer inserted at app entry.

QuitSmokingApp/
├── Models/
│   └── QuitSession.swift       # @Model — quit date, cost inputs, computed stats
├── Views/
│   ├── DashboardView.swift     # stat cards + savings chart
│   ├── SetupView.swift         # onboarding form (quit date, cost)
│   └── MilestonesView.swift    # badge grid (1d / 1w / 1m / 1y)
├── Components/
│   └── StatCard.swift          # reusable metric tile
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define a QuitSession SwiftData model that stores user inputs and exposes smoke-free stats as computed properties so views always read fresh values.

import SwiftData
import Foundation

@Model
final class QuitSession {
    var quitDate: Date
    var cigarettesPerDay: Int
    var costPerPack: Double
    var cigarettesPerPack: Int

    init(quitDate: Date = .now, cigarettesPerDay: Int = 20,
         costPerPack: Double = 12.0, cigarettesPerPack: Int = 20) {
        self.quitDate = quitDate
        self.cigarettesPerDay = cigarettesPerDay
        self.costPerPack = costPerPack
        self.cigarettesPerPack = cigarettesPerPack
    }

    var daysSmokeFree: Int {
        Calendar.current.dateComponents([.day], from: quitDate, to: .now).day ?? 0
    }

    var moneySaved: Double {
        let packsPerDay = Double(cigarettesPerDay) / Double(cigarettesPerPack)
        return packsPerDay * costPerPack * Double(daysSmokeFree)
    }

    var cigarettesAvoided: Int { daysSmokeFree * cigarettesPerDay }
}

2. Core UI — dashboard view

Query the active session with @Query and render three stat cards; if no session exists yet, nudge the user to set their quit date.

struct DashboardView: View {
    @Query private var sessions: [QuitSession]
    @State private var showSetup = false
    private var session: QuitSession? { sessions.first }

    var body: some View {
        NavigationStack {
            if let session {
                ScrollView {
                    VStack(spacing: 20) {
                        StatCard("Days Smoke-Free",
                                 value: "\(session.daysSmokeFree)",
                                 icon: "lungs.fill", tint: .green)
                        StatCard("Money Saved",
                                 value: session.moneySaved
                                     .formatted(.currency(code: "USD")),
                                 icon: "dollarsign.circle.fill", tint: .mint)
                        StatCard("Cigarettes Avoided",
                                 value: "\(session.cigarettesAvoided)",
                                 icon: "nosign", tint: .orange)
                        SavingsChartView(session: session)
                    }
                    .padding()
                }
                .navigationTitle("My Journey")
            } else {
                ContentUnavailableView("Start Your Journey",
                    systemImage: "heart.fill",
                    description: Text("Tap to set your quit date."))
                .onTapGesture { showSetup = true }
            }
        }
        .sheet(isPresented: $showSetup) { SetupView() }
    }
}

3. Progress and savings tracker

Render cumulative savings as a Swift Charts AreaMark, giving users a visual reward curve that grows steeper over time.

import Charts

struct SavingsChartView: View {
    let session: QuitSession

    private var data: [(day: Int, saved: Double)] {
        let days = max(session.daysSmokeFree, 1)
        let packsPerDay = Double(session.cigarettesPerDay) /
                          Double(session.cigarettesPerPack)
        return (0...days).map { d in
            (day: d, saved: packsPerDay * session.costPerPack * Double(d))
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Cumulative Savings").font(.headline)
            Chart(data, id: \.day) { point in
                AreaMark(
                    x: .value("Day", point.day),
                    y: .value("Saved", point.saved)
                )
                .foregroundStyle(.green.gradient)
                LineMark(
                    x: .value("Day", point.day),
                    y: .value("Saved", point.saved)
                )
                .foregroundStyle(.green)
            }
            .chartXAxisLabel("Days smoke-free")
            .chartYAxisLabel("USD")
            .frame(height: 160)
        }
        .padding()
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
    }
}

4. Privacy Manifest setup

Add PrivacyInfo.xcprivacy to your Xcode target (File → New → File → Privacy Manifest) and declare the UserDefaults API type that SwiftData and SwiftUI access internally — App Store Connect will reject your build without it.

<?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/>
  <key>NSPrivacyTrackingDomains</key>
  <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

Gate premium features — extended chart history beyond 7 days, milestone badge unlocks, and daily motivational notifications — behind a StoreKit 2 auto-renewable subscription. Use Product.products(for: ["com.yourapp.premium.monthly"]) to fetch the product, product.purchase() to start checkout, and Transaction.currentEntitlement(for:) at app launch to verify the user's active entitlement without a server. Keep a free tier that shows only the last 7 days of savings history — it keeps the conversion funnel open and avoids the perception that core quit-tracking is paywalled, which can attract negative App Store reviews.

Shipping this faster with Soarias

Soarias scaffolds the QuitSession SwiftData model, DashboardView, StatCard component, and Swift Charts integration from a plain-English prompt in a single shot. It writes PrivacyInfo.xcprivacy with the correct NSPrivacyAccessedAPITypeReasons codes automatically, configures fastlane deliver with your screenshots and App Store metadata, and handles the App Store Connect binary upload — so you never touch the manual upload form.

For a beginner-complexity app like this one, most first-time developers spend 2–4 hours on Xcode project setup, Privacy Manifest research, and App Store Connect configuration before writing a single line of product code. Soarias compresses that overhead to under 15 minutes, leaving your full 1–2 weekend budget for features, UI polish, and testing on a real device.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The $99/year Apple Developer Program membership is required to distribute on TestFlight or the App Store. You can build and run on a simulator for free, but real-device testing of UserNotifications and App Store submission both require an active membership.

How do I submit this to the App Store?

Archive your build in Xcode (Product → Archive), then use the Organizer to distribute it to App Store Connect. You'll need an app record created in App Store Connect first — set the bundle ID, name, category (Health & Fitness), and age rating (4+) before uploading. After upload, attach the build to a new app version and submit for review.

Can users track multiple quit attempts?

Yes — SwiftData supports multiple QuitSession objects. Add a list view that lets users create a new session (e.g. after a relapse) and switch between attempt histories. Use a @Query(sort: \.quitDate, order: .reverse) to always surface the most recent attempt first on the dashboard.

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