1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
# 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
|