```html How to Build a Event Countdown App in SwiftUI (2026)

How to Build an Event Countdown App in SwiftUI

An Event Countdown app lets users track multiple upcoming dates — birthdays, vacations, product launches — with live ticking countdowns displayed as home-screen widgets and list cards. It's an ideal first shipping project: focused scope, no backend, real-time UI, and a clear upgrade path to pro features.

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

Prerequisites

Architecture overview

The app uses a SwiftData model container shared between the main target and a WidgetKit extension via an App Group. CountdownEvent records are fetched with @Query in the main list and read directly from the shared store in the widget's TimelineProvider. A 1-second Timer publisher drives animated digit updates in each card, while the widget's timeline refreshes every 15 minutes or immediately when the user saves a new event.

CountdownApp/
├── App/
│   └── CountdownApp.swift        # @main + modelContainer(appGroup:)
├── Models/
│   └── CountdownEvent.swift      # @Model — title, emoji, targetDate
├── Views/
│   ├── EventListView.swift       # @Query list + Add sheet
│   └── CountdownCard.swift       # Timer + numericText animation
├── Widget/
│   └── CountdownWidget.swift     # TimelineProvider + entry view
└── PrivacyInfo.xcprivacy

Step-by-step

1. Data model

Define a SwiftData @Model that stores each event and exposes a computed secondsRemaining property so every view reads from one source of truth.

import SwiftData
import Foundation

@Model
final class CountdownEvent {
    var title: String
    var emoji: String
    var targetDate: Date
    var colorHex: String
    var createdAt: Date

    init(title: String, emoji: String = "🎉",
         targetDate: Date, colorHex: String = "#5E5CE6") {
        self.title     = title
        self.emoji     = emoji
        self.targetDate = targetDate
        self.colorHex  = colorHex
        self.createdAt = .now
    }

    var isPast: Bool { targetDate < .now }

    var secondsRemaining: Int {
        max(0, Int(targetDate.timeIntervalSinceNow))
    }
}

2. Core UI — event list

Use @Query to fetch events sorted by target date and render each one as a CountdownCard, with swipe-to-delete wired directly to the model context.

import SwiftUI
import SwiftData

struct EventListView: View {
    @Query(sort: \CountdownEvent.targetDate) var events: [CountdownEvent]
    @Environment(\.modelContext) private var context
    @State private var showAdd = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(events) { event in
                    CountdownCard(event: event)
                        .listRowBackground(Color.clear)
                        .listRowSeparator(.hidden)
                        .listRowInsets(.init(top: 6, leading: 16, bottom: 6, trailing: 16))
                }
                .onDelete { idx in idx.forEach { context.delete(events[$0]) } }
            }
            .listStyle(.plain)
            .navigationTitle("Countdowns")
            .toolbar { Button("Add", systemImage: "plus") { showAdd = true } }
            .sheet(isPresented: $showAdd) { AddEventSheet() }
        }
    }
}

3. Multi-event live countdowns

Each card subscribes to a 1-second timer and uses .contentTransition(.numericText()) so digits animate smoothly on every tick — no third-party libraries needed.

struct CountdownCard: View {
    let event: CountdownEvent
    @State private var seconds = 0
    private let ticker = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("\(event.emoji) \(event.title)").font(.headline)
            HStack(spacing: 20) {
                unitView(seconds / 86400,          "days")
                unitView((seconds % 86400) / 3600, "hrs")
                unitView((seconds % 3600) / 60,    "min")
                unitView(seconds % 60,             "sec")
            }
        }
        .padding()
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
        .onReceive(ticker) { _ in withAnimation { seconds = event.secondsRemaining } }
        .onAppear { seconds = event.secondsRemaining }
    }

    @ViewBuilder
    private func unitView(_ value: Int, _ label: String) -> some View {
        VStack(spacing: 2) {
            Text("\(value)").font(.title2.monospacedDigit().bold())
                .contentTransition(.numericText())
                .animation(.easeInOut(duration: 0.3), value: value)
            Text(label).font(.caption2).foregroundStyle(.secondary)
        }
    }
}

4. Privacy Manifest

Add PrivacyInfo.xcprivacy via File › New › Resource › App Privacy File to declare UserDefaults access — Apple requires this for any app whose binary, or whose extension (WidgetKit), reads UserDefaults.

<!-- PrivacyInfo.xcprivacy — add to both app and widget targets -->
<?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: One-time purchase

Implement a one-time unlock with StoreKit 2's Product.purchase() API. Define a non-consumable in-app purchase in App Store Connect (e.g. com.yourapp.countdown.pro), then gate premium features — unlimited events beyond three, custom colors, and Lock Screen widget families — behind a Transaction.currentEntitlement(for:) check at app launch. StoreKit 2 validates receipts server-side automatically so no backend is needed. Cache the entitlement state in UserDefaults for instant UI response, but always re-verify via StoreKit on launch to remain compliant with App Store guidelines and handle refunds gracefully.

Shipping this faster with Soarias

Soarias scaffolds the full project from a single prompt: it generates the CountdownEvent SwiftData model, wires App Group entitlements across both the app and widget targets, produces a working TimelineProvider with correct reload calls, and pre-fills PrivacyInfo.xcprivacy with the right reason codes. Fastlane lanes for screenshot capture and App Store Connect metadata submission are ready to run with one command.

For a beginner-complexity project like this, most solo developers save a full weekend. The WidgetKit App Group configuration — making both targets share one SwiftData store without silent failures — typically takes 2–4 hours to debug by hand. Soarias gets it right on the first scaffold, so you ship the feature instead of chasing entitlement mismatches.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. A free account lets you sideload the app on your own device via Xcode, but you need the $99/year Apple Developer Program to submit to TestFlight or the App Store. You also need a paid account to create App Group identifiers in the developer portal — required for the WidgetKit extension to share data with the main app.

How do I submit this to the App Store?

Archive the app in Xcode (Product › Archive), then upload via Xcode Organizer or xcrun altool. In App Store Connect, complete metadata, privacy nutrition labels, and screenshots — Apple requires 6.7″ and 5.5″ iPhone sizes at minimum. With a simple utility like this, review typically takes 24–48 hours after submission.

Can I show countdowns on the Lock Screen?

Yes. Add .accessoryCircular and .accessoryRectangular to your widget's supportedFamilies. Lock Screen widgets render in a reduced-color environment — mark accent elements with .widgetAccentable() and test on a real device, since the Simulator doesn't fully replicate Lock Screen rendering or tinting behavior.

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

```