How to Build a Period Tracker App in SwiftUI

A period tracker app lets users log menstrual cycles, predict future periods from their personal history, and receive private on-device reminders — no account required. It's a high-retention health utility for people who want a private, local-first alternative to data-hungry mainstream apps.

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

Prerequisites

Architecture overview

Cycle data lives entirely on-device in SwiftData — one CycleEntry record per period. A pure Swift CyclePredictionEngine computes the rolling average cycle length and drives both the home view prediction ring and UserNotifications scheduling. The Swift Charts framework renders a compact bar chart of recent cycle lengths. No server, no analytics SDK, no account — privacy is a feature you can market.

PeriodTrackerApp/
├── Models/
│   ├── CycleEntry.swift           # @Model — persisted cycle records
│   └── FlowIntensity.swift        # Codable enum
├── Views/
│   ├── CycleHomeView.swift        # prediction ring + chart
│   ├── LogEntryView.swift         # log new period sheet
│   └── InsightsView.swift         # full Charts history
└── Engine/
    ├── CyclePredictionEngine.swift # rolling-average logic
    └── ReminderScheduler.swift    # UNUserNotificationCenter

Step-by-step

1. Data model with SwiftData

Define CycleEntry as a SwiftData @Model so every logged period persists automatically — no CoreData boilerplate, no manual saves.

import SwiftData
import Foundation

enum FlowIntensity: String, Codable, CaseIterable {
    case light  = "Light"
    case medium = "Medium"
    case heavy  = "Heavy"
}

@Model
final class CycleEntry {
    var startDate: Date
    var endDate: Date?
    var flowIntensity: FlowIntensity
    var symptoms: [String]
    var notes: String

    init(startDate: Date, flowIntensity: FlowIntensity = .medium) {
        self.startDate     = startDate
        self.flowIntensity = flowIntensity
        self.symptoms      = []
        self.notes         = ""
    }
}

2. Core UI — CycleHomeView

Query all entries sorted by date, surface the next predicted period with a relative-time label, and expose a sheet for logging a new cycle.

struct CycleHomeView: View {
    @Query(sort: \CycleEntry.startDate, order: .reverse)
    private var cycles: [CycleEntry]
    @State private var showLogSheet = false

    var nextPeriod: Date? { CyclePredictionEngine.predict(from: cycles) }

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 24) {
                    PredictionRingView(nextDate: nextPeriod)
                        .frame(height: 200)
                    if let next = nextPeriod {
                        Text("Next period \(next, style: .relative)")
                            .font(.headline).foregroundStyle(.pink)
                    }
                    CycleHistoryChart(cycles: Array(cycles.prefix(6)))
                        .frame(height: 160).padding(.horizontal)
                }
            }
            .navigationTitle("My Cycle")
            .toolbar {
                Button("Log Period", systemImage: "plus.circle.fill") {
                    showLogSheet = true
                }
            }
            .sheet(isPresented: $showLogSheet) { LogEntryView() }
        }
    }
}

3. Cycle tracking and predictions

Compute the rolling average cycle length from all historical entries, fall back to 28 days for new users, then schedule a UNCalendarNotificationTrigger two days before the predicted start.

struct CyclePredictionEngine {
    static func predict(from cycles: [CycleEntry]) -> Date? {
        let sorted = cycles.sorted { $0.startDate < $1.startDate }
        guard !sorted.isEmpty else { return nil }
        guard sorted.count >= 2 else {
            return Calendar.current.date(byAdding: .day, value: 28, to: sorted[0].startDate)
        }
        let lengths: [Int] = zip(sorted, sorted.dropFirst()).map {
            Calendar.current.dateComponents([.day], from: $0.startDate, to: $1.startDate).day ?? 28
        }
        let avg = lengths.reduce(0, +) / lengths.count
        return Calendar.current.date(byAdding: .day, value: avg, to: sorted.last!.startDate)
    }

    static func scheduleReminder(for date: Date) async throws {
        let center = UNUserNotificationCenter.current()
        try await center.requestAuthorization(options: [.alert, .sound])
        let content      = UNMutableNotificationContent()
        content.title    = "Period due soon"
        content.body     = "Your period is predicted to arrive in 2 days."
        let fireDate     = Calendar.current.date(byAdding: .day, value: -2, to: date)!
        let comps        = Calendar.current.dateComponents([.year,.month,.day,.hour], from: fireDate)
        let trigger      = UNCalendarNotificationTrigger(dateMatching: comps, repeats: false)
        try await center.add(UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger))
    }
}

4. Privacy Manifest (PrivacyInfo.xcprivacy)

Add PrivacyInfo.xcprivacy to your app target (not just a framework bundle) — Apple requires it for any submission that collects health or fitness data.

<?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>NSPrivacyTrackingDomains</key><array/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array>
    <dict>
      <key>NSPrivacyCollectedDataType</key>
      <string>NSPrivacyCollectedDataTypeHealthAndFitness</string>
      <key>NSPrivacyCollectedDataTypeLinked</key><false/>
      <key>NSPrivacyCollectedDataTypeTracking</key><false/>
      <key>NSPrivacyCollectedDataTypePurposes</key>
      <array>
        <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyAccessedAPITypes</key><array/>
</dict></plist>

Common pitfalls

Adding monetization: Subscription

Use StoreKit 2's Product.products(for:) to fetch monthly and annual subscription SKUs you've configured in App Store Connect. Gate premium features — multi-cycle insights, symptom trend charts, and CSV export — behind a Transaction.currentEntitlement(for:) check rather than paywalling core logging, which App Store Review will reject. Present the paywall using .subscriptionStoreView(groupID:) for Apple's native subscription UI, which handles payment, restoration, and promotional offers automatically. Frame the subscription around privacy and depth of insights, not access to the tracker itself — that framing converts better and sails through review.

Shipping this faster with Soarias

Soarias handles the scaffolding that burns time on a health app: it generates the SwiftData model layer, wires the ModelContainer into your App entry point, creates a correctly keyed PrivacyInfo.xcprivacy, configures fastlane with your App Store Connect credentials, and submits the build complete with screenshots, metadata, and privacy nutrition labels — all without you touching App Store Connect manually.

For an intermediate project at this complexity, most developers spend two to three days on Xcode project setup, Privacy Manifest research, and ASC configuration before writing a single line of product code. Soarias compresses that to under an hour, so your full week lands on prediction logic, UI polish, and subscription paywall implementation — the parts that actually determine whether your app succeeds.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. A free Apple ID lets you run the app on your own device during development, but distributing via TestFlight or submitting to the App Store requires the $99/year Apple Developer Program. Enroll at developer.apple.com before you start — account activation can take 24–48 hours.

How do I submit this to the App Store?

Archive the app in Xcode via Product → Archive, then upload through Xcode Organizer. In App Store Connect, complete your listing — screenshots for all required device sizes, description, age rating, privacy nutrition labels, and your privacy policy URL. Health-related apps receive careful human review; budget 3–5 business days and be prepared to answer questions about your data handling practices.

Should I integrate HealthKit for cycle data?

HealthKit's HKCategoryTypeIdentifier.menstrualFlow lets users share data with Apple Health and third-party apps, which is a real differentiator. However, it requires a physical device for all testing, an additional entitlement, and HealthKit-specific App Store review scrutiny. For v1, SwiftData-only is faster to ship and easier to reason about. Add HealthKit sync in a follow-up release once your core prediction loop is validated by real users.

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