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.
Prerequisites
- Mac with Xcode 16 or later
- Apple Developer Program ($99/year) — required for TestFlight and App Store distribution
- Basic Swift and SwiftUI knowledge (variables, structs, property wrappers)
- Familiarity with
CalendarandDatefrom Foundation — we will be leaning on these heavily
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
-
Using raw division instead of
Calendar.dateComponents. Dividing aTimeIntervalby 31,536,000 seconds will give wrong results for leap years and birthdays at year boundaries. Always useCalendar. -
Not clamping the DatePicker to past dates. Omitting the
in: ...Date.nowrange allows future birthdates, which produces negative age values and confuses users. Always pass the range parameter. -
Forgetting the Privacy Manifest before submitting. Apple's automated pipeline will reject your binary at upload time — not during review — if
PrivacyInfo.xcprivacyis missing. Add it before you ever hit Archive. -
Using deprecated
UIWebView-based ad SDKs. If you integrate Google Mobile Ads via Swift Package Manager, always pull the latest version tag. Older versions bundle deprecated WebKit entitlements that trigger App Store review flags. -
Timezone mismatches on "today."
Date.nowis in UTC internally; always callCalendar.current(notCalendar(identifier: .gregorian)) so date component calculations respect the user's local timezone.
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
@Observablemacro and the#Previewmacro both require iOS 17+. If you need iOS 16 support, replace@Observablewith@ObservableObject/@Publishedand replace#Previewwith the olderPreviewProviderprotocol. SwiftData also requires iOS 17, so swap it forUserDefaultsor plainCodableJSON 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 = 0is the simplest path — it wrapsUserDefaultsin 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.