# Solstice Countdown Widget & App Design **Date:** 2026-03-23 **Project:** Solverv (Solstice Countdown) **Status:** Design Approved --- ## Overview Build an iOS app that displays a countdown to the next solstice or equinox, with season-specific imagery and detailed information. The app includes: - **Widget Extension:** Compact countdown widget in small, medium, and large sizes - **Main App:** Info screen showing expanded details including sunrise/sunset times and upcoming events --- ## Requirements ### Functional Requirements 1. Track all four annual solstices/equinoxes (Spring, Summer, Autumn, Winter) 2. Display countdown to next event in **days only** 3. Show season-specific images that change based on upcoming event 4. Calculate and display sunrise/sunset times for user's current location 5. Show expanded event details in main app (exact times, season descriptions, upcoming event previews) 6. Support multiple widget sizes (small, medium, large) ### Non-Functional Requirements - All calculations happen locally (no external APIs) - Works offline after initial setup - Respects location privacy with explicit permission - Single location permission prompt (shared via AppGroup) - Data synced between app and widget via AppGroup container --- ## Architecture ### Data Models **SolsticeEvent** ``` - name: String (e.g., "Summer Solstice", "Spring Equinox") - date: Date (UTC) - season: Season (enum: spring, summer, autumn, winter) - seasonDescription: String (brief description of the season) ``` **SolsticeData** ``` - events: [SolsticeEvent] (hardcoded data, 2025-2030) - nextEvent() -> SolsticeEvent (returns next upcoming event) - allUpcoming(count: Int) -> [SolsticeEvent] (returns next N events) - daysUntil(_ event: SolsticeEvent) -> Int - progressToEvent(_ event: SolsticeEvent) -> (elapsed: Int, total: Int) ``` **SunTimes** ``` - latitude: Double - longitude: Double - date: Date - sunrise() -> Date - sunset() -> Date ``` ### Data Sharing **AppGroup Container:** Store user location and cached sunrise/sunset times using the container ID `group.com.ivarlovlie.solverv` **AppGroup Data Schema:** Stored in UserDefaults using the `group.com.ivarlovlie.solverv` container. ```json { "userLocation.latitude": Double (e.g., 59.9139), "userLocation.longitude": Double (e.g., 10.7522), "userLocation.timestamp": String (ISO 8601, e.g., "2026-03-23T10:30:00Z"), "userLocation.isDefaultLocation": Boolean (true = Greenwich, false = user-granted location), "sunTimes.date": String (ISO 8601 date only, e.g., "2026-03-23"), "sunTimes.sunrise": String (ISO 8601 datetime in local timezone, e.g., "2026-03-23T06:42:00"), "sunTimes.sunset": String (ISO 8601 datetime in local timezone, e.g., "2026-03-23T18:15:00"), "sunTimes.timestamp": String (ISO 8601, when calculated, e.g., "2026-03-23T10:30:00Z") } ``` All Date values stored as ISO 8601 strings for cross-process compatibility. - **Widget Timeline:** Updates at midnight (local time) using `timelineReloadPolicy: .after(nextMidnight)` - Both app and widget read from the same AppGroup container for consistency - Cache invalidation: Sunrise/sunset cache is recalculated if stored date differs from today or location changes --- ## Widget Specification ### Small Widget (169×169) - **Layout:** Vertical stack - Seasonal image (fills most of space) - Event name (small caption) - Countdown in days (large, bold text) - **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` where `nextMidnight` is calculated as: ```swift var calendar = Calendar.current var components = calendar.dateComponents([.year, .month, .day], from: Date()) components.hour = 0 components.minute = 0 components.second = 0 let todayMidnight = calendar.date(from: components)! let nextMidnight = calendar.date(byAdding: .day, value: 1, to: todayMidnight)! ``` The widget refreshes daily at midnight local time, automatically accounting for DST transitions. - **Purpose:** Quick glance at how many days remain ### Medium Widget (364×169) - **Layout:** Horizontal split - Left: Seasonal image (square) - Right: Vertical stack with event name, countdown (large), progress bar - **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` — refreshes at midnight local time to update countdown - **Purpose:** Balance of visual and numeric information ### Large Widget (364×364) - **Layout:** Vertical stack - Top half: Seasonal image - Bottom half: Event name, countdown, progress bar, preview of next 3 upcoming events (mini list) - **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` — refreshes at midnight local time to update countdown - **Purpose:** Comprehensive view with upcoming events preview ### Images - One image per season (spring, summer, autumn, winter) - Sourced from Assets.xcassets - Same image shown for all events in that season **Image Specifications:** - **Asset Names:** `SeasonSpring`, `SeasonSummer`, `SeasonAutumn`, `SeasonWinter` (stored in Assets.xcassets) - **Color Set Strategy:** Each season asset has two variants (light and dark mode) using Xcode's Color Set appearance settings - **Aspect Ratio:** 1:1 (square) - **Resolutions per variant:** - 1x: 1024×1024 - 2x: 2048×2048 - 3x: 3072×3072 - **Safe Area:** Ensure important visual content avoids outer 20-point margin (on a 1024×1024 base, keep content within 960×960 center area) - **Format:** PNG with alpha channel - **Fallback:** If image fails to load, display solid color matching the season (spring: #4CAF50, summer: #FFC107, autumn: #FF9800, winter: #2196F3) --- ## Main App Info Screen ### Top Section - Seasonal image (landscape orientation friendly) - Next event name (large) - Countdown in days (very large, prominent) - Progress bar showing days elapsed since the previous solstice/equinox divided by total days until next solstice/equinox - Calculation: `(today - lastSolsticeDate) / (nextSolsticeDate - lastSolsticeDate)` - Example: Winter Solstice was Dec 21, 2025 (passed). Spring Equinox is Mar 20, 2026 (89 days later). On Jan 21, 2026, progress is 31/89 days elapsed. - The progress bar **resets to 0%** the moment a new solstice/equinox occurs ### Middle Section - **Today's Sun Times** - Sunrise time - Sunset time - Calculated from device location - **Season Info** - Season name - Brief description (e.g., "Spring Equinox — Day and night are approximately equal length") ### Bottom Section - **Upcoming Events List** (scrollable) - Shows next 8-12 events - Each row displays: - Event name (e.g., "Spring Equinox") - Date/Time in local timezone, 12-hour format with AM/PM (e.g., "Mar 20, 2026 2:46 PM") - Days remaining as integer (e.g., "45 days") - Season color indicator: 12pt circle matching the season's primary color (spring: green, summer: yellow, autumn: orange, winter: blue) ### Navigation - Tab bar or simple navigation to this screen - Refresh button to manually update sunrise/sunset (in case location changed) --- ## Time Zone & Location Handling ### Location - Request permission on first app launch - Store latitude/longitude in AppGroup container - Fall back to Greenwich/UTC (0°, 0°) if permission denied - User can manually update location in app settings ### Time Zones - All solstice times stored in UTC (as they are now) - Convert to user's local timezone for display - Sunrise/sunset calculated for user's timezone and location ### Sunrise/Sunset Algorithm - Implement a simplified solar position algorithm based on the NOAA algorithm (https://github.com/NOAA-OWP/sunpy) - No external APIs; algorithm is self-contained - **Algorithm Reference:** NOAA Solar Position Algorithm - Calculate solar declination using Spencer's formula - Calculate equation of time for date - Calculate hour angle at sunrise/sunset - Convert to local solar time then UTC - **Inputs:** latitude (Double), longitude (Double), date (Date in user's timezone) - **Outputs:** sunrise (Date), sunset (Date) in local timezone - **Cache:** Store in AppGroup container; recalculate daily or when location changes - **Accuracy:** Results valid to ±2 minutes --- ## Error Handling & Edge Cases ### Location Permission - App requests permission on first launch - If denied, use default location (Greenwich) and notify user - User can grant permission later in system Settings ### Time Zone Edge Cases - Solstice at midnight: Display correctly in both UTC and local time - User crosses timezone: Times update automatically on app launch - Widget timezone: Uses device timezone (set by system) ### Data Integrity - Solstice dates are hardcoded and immutable - Sunrise/sunset cached but recalculated daily - Widget syncs with app via AppGroup on launch --- ## Testing Strategy ### Widget Testing - Preview all three widget sizes with mock solstice data - Verify countdown updates correctly across timezone boundaries - Test image display in different iOS versions (iOS 17 fallback) ### App Testing - Verify sunrise/sunset calculations against known values - Test location permission flows (allowed, denied, not yet asked) - Test data sync between app and widget - Verify time zone conversions for various user locations ### Integration Testing - Widget refreshes daily and displays current countdown - App and widget show consistent "next event" data - Location changes update sunrise/sunset in real time --- ## Implementation Notes ### Existing Code - `SolvervDef` already contains solstice dates (2025-2030) - Widget structure (`Solsnu_Widget.swift`) is scaffolded - Main app has basic SwiftUI structure ready for info screen ### New Components to Build - `SunTimes` calculation (sunrise/sunset) - `SolsticeEvent` model - Widget layout variants (small, medium, large) - Info screen UI - AppGroup data sharing ### Dependencies - None (all calculations are built-in or custom) - WidgetKit (already available) - SwiftUI (already used) --- ## Additional Clarifications ### Solstice Date Coverage - Hardcoded data spans 2025–2030 - After 2030, app continues functioning but upcoming events won't display beyond December 2030 - Plan for data expansion before 2030: extend to 2040+ in an app update ### Season Assignment - The displayed season for an event is always the season being celebrated - Example: Spring Equinox always displays spring imagery (green), even if shown during late winter on the calendar ### Offline Functionality - **First Launch:** - App requests location permission - If granted: Fetch location and calculate sunrise/sunset for that location, cache in AppGroup container - If denied: Use default location (Greenwich, 0°/0°) and cache as `isDefaultLocation: true` - All subsequent uses work offline - **Subsequent Use:** - App and widget work entirely offline; solstice dates and countdown calculations are all local - Sunrise/sunset times use cached values from AppGroup container - **State Recovery on Permission Changes:** - If app is running when user changes location permission in Settings, recalculate sunrise/sunset on next app foreground event - If AppGroup container is missing or corrupted on launch, fall back to Greenwich location and log error - **Widget Behavior When Location Unavailable:** - Widget always displays countdown (never fails) - Sunrise/sunset times display as "—" if location permission denied and no cached data exists --- ## Success Criteria ✅ Widget displays countdown in days ✅ Seasonal image changes based on next event ✅ All four solstices/equinoxes tracked ✅ Sunrise/sunset times calculated from location ✅ Info screen shows all requested details ✅ App and widget data stay in sync ✅ Works offline after initial setup ✅ All three widget sizes render correctly