```html How to Build a Medication Reminder App in SwiftUI (2026)

How to Build a Medication Reminder App in SwiftUI

A Medication Reminder app lets users set up repeating pill schedules, get notified at the right time each day, and log whether they took each dose. It's built for people managing chronic conditions or caregivers keeping family members on track.

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

Prerequisites

Architecture overview

The app is entirely local-first: SwiftData persists Medication and MedSchedule records on-device with no backend needed. A thin NotificationScheduler service bridges SwiftData models to UserNotifications, scheduling a UNCalendarNotificationTrigger for each weekday in each schedule. SwiftUI views powered by @Query stay in sync with the model context automatically, and a StoreKit 2 paywall gates premium features behind a subscription.

MedicationReminder/
├── Models/
│   ├── Medication.swift           # @Model: name, dosage, colorHex, isActive
│   └── MedSchedule.swift          # @Model: time, weekdays, takenDates
├── Views/
│   ├── MedicationListView.swift   # @Query list + Add sheet
│   ├── AddMedicationView.swift    # Form with DatePicker + weekday toggles
│   └── MedDetailView.swift        # Dose history + taken toggle
├── Services/
│   └── NotificationScheduler.swift
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define Medication and MedSchedule as SwiftData @Model classes with a cascade-delete relationship so removing a medication also removes all its schedules.

import SwiftData
import Foundation

@Model final class Medication {
    var name: String
    var dosage: String
    var colorHex: String
    var isActive: Bool
    @Relationship(deleteRule: .cascade)
    var schedules: [MedSchedule] = []
    init(name: String, dosage: String, colorHex: String = "#4F8EF7") {
        self.name = name; self.dosage = dosage
        self.colorHex = colorHex; self.isActive = true
    }
}

@Model final class MedSchedule {
    var time: Date          // only hour + minute are used
    var weekdays: [Int]     // 1 = Sun … 7 = Sat (Calendar weekday convention)
    var takenDates: [Date] = []
    init(time: Date, weekdays: [Int]) {
        self.time = time; self.weekdays = weekdays
    }
}

2. Core UI

Drive MedicationListView with @Query so insertions and deletions reflect instantly without manual state management.

import SwiftUI
import SwiftData

struct MedicationListView: View {
    @Query(sort: \Medication.name) private var meds: [Medication]
    @Environment(\.modelContext) private var context
    @State private var showAdd = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(meds) { med in
                    NavigationLink(value: med) {
                        Label(med.name, systemImage: "pill.fill")
                            .badge(med.schedules.count)
                    }
                }
                .onDelete { idx in idx.forEach { context.delete(meds[$0]) } }
            }
            .navigationTitle("Medications")
            .navigationDestination(for: Medication.self) { MedDetailView(medication: $0) }
            .toolbar { ToolbarItem(placement: .primaryAction) {
                Button("Add", systemImage: "plus") { showAdd = true }
            }}
            .sheet(isPresented: $showAdd) { AddMedicationView() }
        }
    }
}

3. Pill schedule notifications

Schedule a repeating UNCalendarNotificationTrigger per weekday per schedule, using .timeSensitive interruption level so alerts surface through Focus modes when it matters most.

import UserNotifications

@MainActor final class NotificationScheduler {
    static let shared = NotificationScheduler()
    private let center = UNUserNotificationCenter.current()

    func requestAuth() async -> Bool {
        (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
    }

    func reschedule(for med: Medication) async {
        let prefix = "\(med.persistentModelID.hashValue)"
        let ids = (await center.pendingNotificationRequests())
            .filter { $0.identifier.hasPrefix(prefix) }.map(\.identifier)
        center.removePendingNotificationRequests(withIdentifiers: ids)
        guard med.isActive else { return }
        for sched in med.schedules {
            for day in sched.weekdays {
                let c = UNMutableNotificationContent()
                c.title = "Take \(med.name)"; c.body = med.dosage
                c.sound = .default; c.interruptionLevel = .timeSensitive
                var dc = Calendar.current.dateComponents([.hour, .minute], from: sched.time)
                dc.weekday = day
                try? await center.add(UNNotificationRequest(
                    identifier: "\(prefix)-\(day)", content: c,
                    trigger: UNCalendarNotificationTrigger(dateMatching: dc, repeats: true)))
            }
        }
    }
}

4. Privacy Manifest (PrivacyInfo.xcprivacy)

Add a PrivacyInfo.xcprivacy file to your Xcode target — App Store Connect rejects submissions that access required-reason APIs without a declared manifest.

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

Use StoreKit 2's Product.products(for:) to load a monthly or annual subscription configured in App Store Connect, then gate premium features — multiple family member profiles, a PDF dose-history export, a home screen widget — behind a Transaction.currentEntitlement(for:) check. Present the paywall with SubscriptionStoreView (iOS 17+), which handles purchasing, restoration, and introductory offer display with minimal boilerplate. Attach a Transaction.updates listener at app launch so renewals and refunds are reflected immediately without requiring the user to reopen the app.

Shipping this faster with Soarias

Soarias scaffolds the full project from your concept in a single Claude Code session — SwiftData models with the cascade-delete relationship wired up, NotificationScheduler, the PrivacyInfo.xcprivacy manifest pre-filled for UserDefaults access, and a SubscriptionStoreView paywall stub. It then generates the fastlane Deliverfile, captures App Store screenshots across the required 6.9″ and 6.5″ device sizes, and populates all required App Store Connect metadata so you're not blocked by review-prep busywork.

For an intermediate project at this complexity, most developers lose two to three days on notification scheduling edge cases and another day on App Store Connect setup and screenshot production. With Soarias handling that scaffolding and submission pipeline, a realistic calendar estimate drops from a full week to a long weekend.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. The $99/year Apple Developer Program membership is required for TestFlight distribution and App Store submission. You can run the app on your own device for free using a personal team, but that provisioning profile expires every 7 days and cannot be used for external testing.

How do I submit this to the App Store?

Archive in Xcode via Product → Archive, then validate and distribute through Organizer to App Store Connect. You'll need screenshots for iPhone 6.9″ and 6.5″ displays, a support URL, a privacy policy URL (required when you handle health-adjacent data), and completed privacy nutrition labels in App Store Connect before Apple will start reviewing.

Can users sync their medication data across devices with iCloud?

Yes — SwiftData supports CloudKit sync with minimal code changes. Enable the iCloud and CloudKit capabilities in your target, then pass ModelConfiguration(cloudKitDatabase: .automatic) to your ModelContainer. All @Model properties must be optional or have defaults for CloudKit compatibility. Because medication records are sensitive, update your privacy policy and App Store privacy nutrition labels to reflect that this data is synced across the user's own devices.

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

```