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
|
# Sunrise/Sunset Times Widget Feature
**Date:** 2026-03-23
**Status:** Design Approved
## Overview
Add sunrise and sunset time display to both small and medium widgets. Times are calculated locally using the existing NOAA solar position algorithm, formatted as HH:mm, and updated daily at midnight based on user location.
## Requirements
- Display sunrise/sunset times in HH:mm format on widgets
- Calculate times locally using existing `SunTimes` utility
- Use user's current location for calculations
- Update once daily at midnight (aligned with current refresh policy)
- Show nothing if location permission is denied or unavailable
- Apply to both small and medium widget families
## Architecture
### Data Model
Extend `SolvervDef` to include sunrise/sunset times:
```
SolvervDef
├── date: Date
├── bg: String
├── sunriseTime: Date? (optional)
├── sunsetTime: Date? (optional)
├── sunriseFormatted: String (computed)
└── sunsetFormatted: String (computed)
```
**Constructor signature:**
```swift
init(date: Date, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
self.date = date
self.sunriseTime = sunriseTime
self.sunsetTime = sunsetTime
self.bg = "smallbg"
}
```
The formatted properties extract HH:mm in device local time.
### Timeline Provider Logic
Update `Provider.getTimeline()`:
1. Get user's cached location from `AppGroupManager` (widget must use App Group storage, not LocationManager)
2. Check if location is recent (less than 24 hours old); if stale/unavailable → create entry with nil sunrise/sunset
3. Initialize local `SunTimes(latitude, longitude, date: currentDate)` calculator
4. Extract sunrise and sunset times (already converted to device local time)
5. Create entry with times: `SolvervEntry(def: SolvervDef(date: currentDate, sunriseTime: sr, sunsetTime: ss))`
6. Return timeline refreshing at next midnight
**Note:** Widget runs in separate process and cannot access main app's LocationManager. Location MUST come from AppGroupManager storage, populated by the main app.
### Data Calculation Flow
```
Location Permission Check
↓ (if granted)
Get Cached Coordinates
↓
SunTimes Calculator
├─ Input: latitude, longitude, date
├─ Output: sunrise Date, sunset Date
└─ Fallback: nil (polar regions, edge cases)
↓
Format as HH:mm
↓
SolvervDef/Entry
↓
Widget Display
```
### Error Handling
| Scenario | Behavior |
|----------|----------|
| Location permission denied | No sunrise/sunset displayed |
| Location unavailable in AppGroupManager | No sunrise/sunset displayed |
| Location older than 24 hours | Treat as unavailable; no sunrise/sunset displayed |
| SunTimes returns nil (polar regions, 24h sun/darkness) | No sunrise/sunset displayed |
### Timezone Handling
- `SunTimes` internally converts to device's local timezone (uses `TimeZone.current`)
- Sunrise/sunset `Date` objects are in device local time
- Formatted display uses device local time (HH:mm)
- **Edge case:** If user changes timezone between midnight and next refresh, sun times remain valid for the previous timezone. They will update correctly at next midnight refresh (when new location may have different timezone). This is acceptable since widget only refreshes daily.
## Implementation Details
### Changes to SolvervDef
Add properties:
- `sunriseTime: Date?` — raw sunrise time in device local timezone
- `sunsetTime: Date?` — raw sunset time in device local timezone
Add computed properties for formatted times:
- `sunriseFormatted: String` — returns "HH:mm" format or empty string if nil
- `sunsetFormatted: String` — returns "HH:mm" format or empty string if nil
Computed properties use device locale and 24-hour format.
### Changes to Provider
Modify `getTimeline()` to:
1. Fetch location from `AppGroupManager.getUserLocation()` — widget runs in separate process and must use App Group storage
2. Check timestamp: if location is older than 24 hours, treat as unavailable
3. If available, instantiate sunrise/sunset calculator: `SunTimes(latitude: lat, longitude: lon, date: currentDate)` from `/Solverv/Utilities/SunTimes.swift`
4. Call `calculator.sunrise()` and `calculator.sunset()` to get Date objects
5. Pass times to `SolvervDef(date: currentDate, sunriseTime: sr, sunsetTime: ss)`
6. Create entry and return timeline refreshing at next midnight (unchanged)
### Widget View Updates
**SmallWidgetView:**
- If sunrise/sunset available: display as "↑ HH:mm ↓ HH:mm" or similar compact format
- If unavailable: display nothing (no placeholder or empty space)
**MediumWidgetView:**
- If sunrise/sunset available: display sunrise and sunset times in HH:mm format (exact layout TBD based on available space)
- If unavailable: display nothing (no placeholder or empty space)
Both views:
- Access times via `entry.def.sunriseFormatted` and `entry.def.sunsetFormatted`
- Only render if both are non-empty strings
## Timeline & Refresh
- **Frequency:** Once per day at midnight
- **Rationale:** Sun times change gradually; daily updates are sufficient
- **Alignment:** Matches existing solstice countdown refresh policy
## Testing
- Test with various locations (tropics, temperate, near poles)
- Test without location permission
- Test at date boundaries (midnight refresh)
- Verify formatted times display correctly
- Verify nil times result in no display
## Future Considerations
- Visualization of sunrise/sunset (charts, gradients)
- Daylight duration calculation and trend
- Multiple locations support
- Custom location override
|