How to Build a Calorie Tracker App in SwiftUI

A calorie tracker lets users log meals, search a live nutrition database, and visualise daily intake against a personal goal — all stored locally on device. It's the definitive health-app portfolio project for iOS developers who want real HealthKit and Charts experience.

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

Prerequisites

Architecture overview

The data layer uses SwiftData to persist FoodEntry records in an on-device SQLite store; @Query and @Environment(\.modelContext) handle all state — no separate view models required for most screens. Nutrition search hits the Open Food Facts public JSON API at runtime and maps responses into transient NutritionItem value types before the user confirms a log entry. HealthKit writes confirmed calorie totals to the Active Energy store so Fitness and other apps see the data. The Charts framework renders per-meal bar charts and a daily progress gauge inline in the dashboard.

CalorieTracker/
├── Models/
│   ├── FoodEntry.swift          # SwiftData @Model
│   └── NutritionItem.swift      # Transient struct from API
├── Views/
│   ├── DashboardView.swift      # Charts + daily summary
│   ├── FoodLogListView.swift    # Meal-grouped log
│   └── SearchFoodView.swift     # Open Food Facts search
├── Services/
│   └── HealthKitManager.swift   # HKHealthStore write
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define FoodEntry as a SwiftData @Model so every logged item is automatically persisted to the on-device store with zero boilerplate.

import SwiftData
import Foundation

enum MealType: String, Codable, CaseIterable {
    case breakfast, lunch, dinner, snack
}

@Model
final class FoodEntry {
    var id: UUID = UUID()
    var name: String
    var calories: Double
    var protein: Double
    var carbs: Double
    var fat: Double
    var servingGrams: Double
    var mealType: MealType
    var loggedAt: Date = Date()

    init(name: String, calories: Double, protein: Double,
         carbs: Double, fat: Double,
         servingGrams: Double = 100,
         mealType: MealType = .lunch) {
        self.name = name; self.calories = calories
        self.protein = protein; self.carbs = carbs
        self.fat = fat; self.servingGrams = servingGrams
        self.mealType = mealType
    }
}

2. Core UI — Dashboard

Render today's calorie total against the user's goal with a Gauge and a per-meal BarMark chart, both driven live by a filtered @Query.

import SwiftUI
import SwiftData
import Charts

struct DashboardView: View {
    @Query(filter: #Predicate<FoodEntry> {
        $0.loggedAt >= Calendar.current.startOfDay(for: .now)
    }, sort: \FoodEntry.loggedAt) private var todayEntries: [FoodEntry]

    let goal: Double = 2000
    var total: Double { todayEntries.reduce(0) { $0 + $1.calories } }

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 28) {
                    Gauge(value: total, in: 0...goal) {
                        Text("kcal").font(.caption)
                    } currentValueLabel: {
                        Text(Int(total), format: .number).font(.title2.bold())
                    }
                    .gaugeStyle(.accessoryCircular)
                    .scaleEffect(2.4).frame(height: 150)

                    Chart(todayEntries) { entry in
                        BarMark(x: .value("Meal", entry.mealType.rawValue.capitalized),
                                y: .value("kcal", entry.calories))
                        .foregroundStyle(by: .value("Meal", entry.mealType.rawValue))
                    }
                    .frame(height: 180).padding(.horizontal)
                }
                .padding(.top, 24)
            }
            .navigationTitle("Today")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    NavigationLink(destination: SearchFoodView()) {
                        Image(systemName: "plus.circle.fill").font(.title2)
                    }
                }
            }
        }
    }
}

3. Food logging with nutrition database

Search Open Food Facts via its public JSON endpoint and insert a FoodEntry into the model context the moment the user confirms a result.

import SwiftUI
import SwiftData

struct SearchFoodView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss
    @State private var query = ""
    @State private var results: [NutritionItem] = []
    @State private var isLoading = false

    var body: some View {
        List(results) { item in
            Button { logFood(item) } label: {
                VStack(alignment: .leading, spacing: 2) {
                    Text(item.name).font(.headline)
                    Text("\(Int(item.kcalPer100g)) kcal · P \(Int(item.protein))g · C \(Int(item.carbs))g · F \(Int(item.fat))g")
                        .font(.caption).foregroundStyle(.secondary)
                }
            }
        }
        .searchable(text: $query, prompt: "Search foods…")
        .task(id: query) {
            guard query.count > 2 else { results = []; return }
            isLoading = true
            results = (try? await OpenFoodFactsService.search(query)) ?? []
            isLoading = false
        }
        .overlay { if isLoading { ProgressView() } }
        .navigationTitle("Add Food")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .cancellationAction) {
                Button("Cancel") { dismiss() }
            }
        }
    }

    private func logFood(_ item: NutritionItem) {
        modelContext.insert(FoodEntry(name: item.name, calories: item.kcalPer100g,
                                     protein: item.protein, carbs: item.carbs, fat: item.fat))
        dismiss()
    }
}

4. Privacy Manifest setup

Add PrivacyInfo.xcprivacy to your Xcode target declaring HealthKit data collection and UserDefaults access — both are required by App Store review.

<?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>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array><string>CA92.1</string></array>
    </dict>
  </array>
  <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>
</dict>
</plist>

Common pitfalls

Adding monetization: Subscription

Use StoreKit 2's Product.products(for:) to fetch monthly and annual SKUs you've configured in App Store Connect. Gate premium features — custom macro goals, weekly trend charts, and CSV export — behind a check on Transaction.currentEntitlements at app launch, writing the result to @AppStorage("isPremium"). Surface the paywall as a .sheet when a free user taps a locked feature. Apple's subscription guidelines require the free tier to deliver genuine value, so keep basic calorie logging (without macro breakdown) fully free to avoid a guideline 3.1.2 rejection during review.

Shipping this faster with Soarias

Soarias scaffolds the full project in the first generation pass — SwiftData model, HealthKit entitlement pre-wired, and PrivacyInfo.xcprivacy with the correct health-data collection declarations already filled in. It then configures fastlane for automated screenshot capture across iPhone 16 Pro and iPhone SE, generates your App Store Connect listing (name, subtitle, keywords, privacy URL), and drives the final binary upload and submission via the ASC API — no web portal required.

For an intermediate project like this one, HealthKit entitlement setup, Privacy Manifest authoring, fastlane lane configuration, and ASC metadata entry typically consume 2–3 full days before you write a single line of product code. Soarias compresses that to under an hour, leaving your entire week for the parts that actually differentiate your app: food search UX, macro charts, and paywall design.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. A free account lets you sideload the app onto your own device via Xcode, but the HealthKit entitlement, TestFlight distribution, and App Store submission all require the $99/year Apple Developer Program membership.

How do I submit this to the App Store?

Archive in Xcode (Product → Archive) and upload via Xcode Organizer or xcrun altool. Then complete the App Store Connect listing — screenshots at required sizes, privacy nutrition labels declaring health data, subscription pricing tiers, and a privacy policy URL — before submitting for review. Health-category apps typically see a 24–48 hour review window.

Can I use a different nutrition database instead of Open Food Facts?

Yes. The USDA FoodData Central API is a free alternative with higher data quality for US products and requires only a free API key. Edamam and Nutritionix offer commercial APIs with more consistent international data — worth the cost if your subscription pricing can absorb it and your target audience is global.

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