```html How to Build an Age Calculator App in SwiftUI (2026)

How to Build an Age Calculator App in SwiftUI

An Age Calculator app lets users enter any birthdate and instantly see their exact age in years, months, and days — plus upcoming birthday countdowns. It's a perfect first SwiftUI project for developers who want to ship something real to the App Store without wrestling complex backends.

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

Prerequisites

Architecture overview

This app is deliberately thin. A single @Observable view model holds the selected birthdate and exposes computed properties that run Calendar.dateComponents against Date.now — no network calls, no async work. The SwiftData layer is optional but useful if you want to let users save multiple birthdays (a family birthdate book). The only external surface is Google Mobile Ads (via Swift Package Manager) for the ad-supported monetization tier; everything else is pure Apple SDK.

AgeCalculatorApp/
├── AgeCalculatorApp.swift        # @main entry, .modelContainer setup
├── Model/
│   └── SavedBirthday.swift       # @Model — name + date
├── ViewModel/
│   └── AgeViewModel.swift        # @Observable — date arithmetic
├── Views/
│   ├── ContentView.swift         # Tab root
│   ├── CalculatorView.swift      # DatePicker + result card
│   └── SavedBirthdaysView.swift  # SwiftData list of saved dates
├── Components/
│   └── AgeResultCard.swift       # Reusable result display
├── Resources/
│   └── PrivacyInfo.xcprivacy     # App Store required
└── AgeCalculatorApp.xcodeproj
      

Step-by-step

1. Create the Xcode project

Open Xcode 16, choose File › New › Project, pick the iOS App template, set the interface to SwiftUI, and enable SwiftData storage. Name it AgeCalculatorApp with a bundle ID like com.yourname.agecalculator. Delete the generated Item.swift model — we will write our own.

// AgeCalculatorApp.swift
import SwiftUI
import SwiftData

@main
struct AgeCalculatorApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: SavedBirthday.self)
    }
}

2. Define the data model with SwiftData

Create SavedBirthday.swift inside a Model group. The @Model macro tells SwiftData to persist this type automatically — no Core Data boilerplate needed.

// Model/SavedBirthday.swift
import Foundation
import SwiftData

@Model
final class SavedBirthday {
    var name: String
    var birthdate: Date

    init(name: String, birthdate: Date) {
        self.name = name
        self.birthdate = birthdate
    }
}

3. Build the core UI with DatePicker

CalculatorView is the heart of the app. A DatePicker restricted to dates in the past feeds into the view model, and an AgeResultCard displays the computed result. Use .datePickerStyle(.graphical) for the calendar wheel — it performs better than .wheel on iPhone 16 screen sizes.

// Views/CalculatorView.swift
import SwiftUI

struct CalculatorView: View {
    @State private var viewModel = AgeViewModel()

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 24) {
                    DatePicker(
                        "Birthdate",
                        selection: $viewModel.birthdate,
                        in: ...Date.now,
                        displayedComponents: .date
                    )
                    .datePickerStyle(.graphical)
                    .padding(.horizontal)

                    AgeResultCard(result: viewModel.ageResult)
                        .padding(.horizontal)
                }
                .padding(.top)
            }
            .navigationTitle("Age Calculator")
        }
    }
}

#Preview {
    CalculatorView()
}

4. Implement date arithmetic in the view model

This is the app's core feature. Calendar.dateComponents(_:from:to:) handles all the tricky edge cases — leap years, month-end rollovers, timezone offsets — so you don't have to do manual division. Mark the class @Observable (Swift 5.9+) so SwiftUI re-renders whenever birthdate changes.

// ViewModel/AgeViewModel.swift
import Foundation
import Observation

struct AgeResult {
    let years: Int
    let months: Int
    let days: Int
    let nextBirthdayDays: Int
}

@Observable
final class AgeViewModel {
    var birthdate: Date = Calendar.current.date(
        byAdding: .year, value: -25, to: .now
    ) ?? .now

    var ageResult: AgeResult {
        let calendar = Calendar.current
        let now = Date.now
        let components = calendar.dateComponents(
            [.year, .month, .day],
            from: birthdate,
            to: now
        )
        let years = components.year ?? 0
        let months = components.month ?? 0
        let days = components.day ?? 0

        // Next birthday countdown
        var nextBirthday = calendar.nextDate(
            after: now,
            matching: calendar.dateComponents([.month, .day], from: birthdate),
            matchingPolicy: .nextTimePreservingSmallerComponents
        ) ?? now
        let daysUntil = calendar.dateComponents(
            [.day], from: now, to: nextBirthday
        ).day ?? 0

        return AgeResult(
            years: years,
            months: months,
            days: days,
            nextBirthdayDays: daysUntil
        )
    }
}

4b. Wire up the result card component

Extract the result display into its own AgeResultCard view. Keeping it separate makes it easy to reuse inside the Saved Birthdays list later and keeps CalculatorView readable.

// Components/AgeResultCard.swift
import SwiftUI

struct AgeResultCard: View {
    let result: AgeResult

    var body: some View {
        VStack(spacing: 16) {
            HStack(spacing: 0) {
                statBlock(value: result.years, label: "Years")
                Divider().frame(height: 48)
                statBlock(value: result.months, label: "Months")
                Divider().frame(height: 48)
                statBlock(value: result.days, label: "Days")
            }
            .frame(maxWidth: .infinity)

            if result.nextBirthdayDays > 0 {
                Text("🎂 Next birthday in \(result.nextBirthdayDays) days")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            } else {
                Text("🎉 Happy Birthday!")
                    .font(.subheadline.bold())
                    .foregroundStyle(.orange)
            }
        }
        .padding()
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
    }

    @ViewBuilder
    private func statBlock(value: Int, label: String) -> some View {
        VStack(spacing: 4) {
            Text("\(value)")
                .font(.system(size: 40, weight: .bold, design: .rounded))
            Text(label)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity)
    }
}

#Preview {
    AgeResultCard(result: AgeResult(years: 28, months: 3, days: 14, nextBirthdayDays: 48))
        .padding()
}

5. Add the Privacy Manifest (required for App Store)

Apple requires a PrivacyInfo.xcprivacy file in every new app submission as of spring 2024. For an age calculator that collects no personal data and makes no network calls, the manifest is minimal — but it must be present or your build will be rejected. In Xcode: File › New › File from Template, search for App Privacy, and select it. Then fill in the values below.

<!-- Resources/PrivacyInfo.xcprivacy -->
<?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>NSPrivacyCollectedDataTypes</key>
  <array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyTrackingDomains</key>
  <array/>
</dict>
</plist>

Common pitfalls

Adding monetization: Ad-supported

The fastest path for a utility like this is a banner ad below the result card, served via the Google Mobile Ads Swift SDK. Add the package in Xcode under File › Add Package Dependencies using the URL https://github.com/googleads/swift-package-manager-google-mobile-ads. Declare your GADApplicationIdentifier in Info.plist, then drop a BannerView (a lightweight UIViewRepresentable wrapper around GADBannerView) at the bottom of CalculatorView. If you want a no-ads upgrade path, pair it with a StoreKit 2 Product.purchase(_:) call gated on a currentEntitlements check — a common pattern is to hide the banner when the user holds a valid non-consumable purchase named com.yourname.agecalculator.removeads. Declare that product in App Store Connect under your app's In-App Purchases tab before submitting.

Shipping this faster with Soarias

Soarias automates the parts of this project that eat time without adding features. After you describe the app in the prompt panel, Soarias scaffolds the Xcode project with the correct SwiftData container, generates the PrivacyInfo.xcprivacy file with the right API access reasons already filled in, and configures a fastlane Fastfile with gym (build) and deliver (ASC upload) lanes. It also writes the App Store Connect metadata — app description, keywords, and screenshot captions — so you are not staring at a blank text field at 11 p.m. before a launch.

For a beginner-complexity app like this one, most developers report saving a full weekend of setup and submission friction. The one-time $79 price pays for itself the first time you skip manually wrestling with fastlane credentials and ASC API keys.

Related guides

FAQ

Does this work on iOS 16?
The @Observable macro and the #Preview macro both require iOS 17+. If you need iOS 16 support, replace @Observable with @ObservableObject / @Published and replace #Preview with the older PreviewProvider protocol. SwiftData also requires iOS 17, so swap it for UserDefaults or plain Codable JSON if you must support iOS 16.
Do I need a paid Apple Developer account to test this on a real device?
No — you can sideload to your own device for free using a personal team in Xcode. However, the free tier limits you to three active apps at a time and certificates expire after seven days, forcing a re-sign. For TestFlight or App Store distribution you do need the $99/year paid program.
How do I submit this to the App Store?
Archive the app in Xcode (Product › Archive), upload via the Organizer, then log into App Store Connect to fill in metadata, screenshots, and pricing. Set the app's Privacy Nutrition Labels to "No data collected" — which is accurate for this app. Submit for review; Apple typically responds within 24–48 hours for a simple utility.
I'm a beginner — can I skip SwiftData and just use UserDefaults?
Absolutely. If you only need to remember a single birthdate, @AppStorage("birthdate") private var birthdateTimestamp: Double = 0 is the simplest path — it wraps UserDefaults in a SwiftUI-friendly property wrapper with zero boilerplate. Use SwiftData only if you want to store multiple named birthdays.

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

```