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
|
# Solverv — Agent Reference
Norwegian solstice and equinox countdown app for iOS, with a WidgetKit extension.
Built with SwiftUI, SwiftData, CoreLocation, and WidgetKit.
## Repository layout
```
Solverv/ Main app target
SolvervApp.swift @main entry point, LocationManager
ContentView.swift Primary UI (struct is ContentView; file header says InfoScreenView)
Item.swift SwiftData @Model stub (Xcode template remnant, not used by UI)
Assets.xcassets/
Shared/ Code compiled into both the app and widget targets
Models/
SolsticeData.swift Singleton; hardcoded events 2025–2030; query API
SolsticeEvent.swift Identifiable+Codable struct; daysUntil(), localDateTime()
Season.swift Enum: spring/summer/autumn/winter; color, assetName, fromDate()
Utilities/
SunTimes.swift NOAA solar algorithm (zenith 90.833°)
AppGroupManager.swift App Group bridge (location + sun times)
Solsnu.Widget/ WidgetKit extension
Solsnu_Widget.swift Provider, SolvervEntry, Solsnu_Widget config, SolvervDef view-model
Views/
SmallWidgetView.swift
MediumWidgetView.swift
SolverVTests/ XCTest suite
Models/SolsticeDataTests.swift
Utilities/SunTimesTests.swift, AppGroupManagerTests.swift
Solverv.xcodeproj/ Xcode project; no SPM Package.swift, no Podfile
```
## Key types and contracts
### `SolsticeData` (Shared/Models/)
Singleton (`SolsticeData.shared`). Stores hardcoded UTC events from 2025 through 2030,
sorted ascending. All query methods take an optional `now: Date` (defaults to `Date()`).
- `nextEvent(from:) -> SolsticeEvent?` — first event with `date > now`
- `upcomingEvents(count:from:) -> [SolsticeEvent]` — next N events
- `progressToNextEvent(from:) -> (elapsed: Int, total: Int)?` — day counts between
the previous and next event; used for progress bars
**Coverage ends at 2030.** Agents adding future events must follow the existing pattern:
`SolsticeEvent(name: "<Norwegian name> <year>", date: SolsticeData.dateFromUTC(...), season: .<season>)`.
### `SolsticeEvent` (Shared/Models/)
`Identifiable, Codable`. UUID is generated at init (not stable across serialization rounds).
- `daysUntil(from:) -> Int` — calendar-day difference, floored at 0
- `localDateTime() -> Date` — **known bug**: adds UTC offset as raw seconds into
DateComponents rather than using a proper timezone-aware conversion. Do not expand
callers of this method without fixing it first.
### `Season` (Shared/Models/)
`fromDate(_:)` uses meteorological (month-based) seasons, not astronomical ones.
`assetName` returns `"Season\(displayName)"` — maps to named color sets in Assets.xcassets.
### `SunTimes` (Shared/Utilities/)
Pure value type. Implements the NOAA solar position algorithm (zenith = 90.833° to
account for atmospheric refraction + solar disk radius). Returns `nil` for polar
night/midnight-sun conditions (cosH out of [-1, 1] range). No caching; callers
instantiate per-date.
```swift
let st = SunTimes(latitude: lat, longitude: lon, date: date)
let sunrise = st.sunrise() // Date? in device local time
let sunset = st.sunset() // Date? in device local time
```
### `AppGroupManager` (Shared/Utilities/)
Singleton (`AppGroupManager.shared`). App Group ID: `group.com.ivarlovlie.solverv`.
Bridges data between the app and widget via `UserDefaults(suiteName:)`.
Two stored payloads:
- `"userLocation"` → `UserLocation` (lat, lon, ISO8601 timestamp, isDefaultLocation)
- `"sunTimes"` → `SunTimes` (date string, ISO8601 sunrise/sunset strings, timestamp)
The widget treats location as stale after 24 hours. Sun times are cached separately
but the widget recomputes them from the stored location rather than reading the cached
sun times struct (the `"sunTimes"` key is written by the app but not read by the widget).
### `SolvervDef` (Solsnu.Widget/Solsnu_Widget.swift)
View-model struct used by widget views. Wraps `SolsticeData` and `SunTimes` queries.
`init(utcString:)` force-unwraps the date parse — acknowledged in a comment, only used
in preview code. Do not use this initializer in production paths.
### `LocationManager` (Solverv/SolvervApp.swift)
`CLLocationManagerDelegate`, ObservableObject. Requests `whenInUse` authorization,
calls `startUpdatingLocation()`, saves to `AppGroupManager` on first fix, then stops.
Does not handle permission denial or restricted state beyond silent no-op.
## Data flow
```
[CLLocationManager] --location--> [LocationManager]
|
AppGroupManager.saveLocation()
AppGroupManager.saveSunTimes()
|
UserDefaults (App Group)
|
+------------------+------------------+
| |
[ContentView] [Widget Provider]
reads location, recomputes reads location, recomputes
SunTimes locally on loadData() SunTimes in getTimeline()
(every 60s via Timer) (refreshes at next midnight)
```
## Build and test
This is a pure Xcode project. There is no `xcodebuild` wrapper script.
Run unit tests from Xcode (Product → Test) or via:
```
xcodebuild test \
-project Solverv.xcodeproj \
-scheme Solverv \
-destination 'platform=iOS Simulator,name=iPhone 16'
```
Tests import the main target with `@testable import Solverv`. The widget target is not
separately testable via the current test scheme.
## Conventions
- Norwegian event names in `SolsticeData`: Vårjevndøgn, Sommersolverv, Høstjevndøgn, Vintersolverv.
- All astronomical times in `SolsticeData` are UTC; `SunTimes` output is device local time.
- `Season.fromDate(_:)` is calendar-month-based; it does not agree with the astronomical
season encoded in `SolsticeEvent.season`. These are intentionally different.
- Widget timeline policy: `.after(nextMidnight)` — one entry per day.
- `SolvervDef` is the single source of truth for widget view logic; views must not
call `SolsticeData` or `SunTimes` directly.
## Known issues / landmines
1. `SolsticeEvent.localDateTime()` applies UTC offset as raw seconds — produces wrong
dates in non-UTC zones with non-whole-hour offsets (e.g., India, Nepal, some AU zones).
2. `AppGroupManager.SunTimes` (the cached struct) is written by the app but the widget
ignores it and recomputes from the cached location. The two caches can diverge.
3. `SolvervDef.init(utcString:)` force-unwraps — preview-only; never call from production.
4. `Item` (SwiftData model) is wired into `SolvervApp.sharedModelContainer` but unused.
Removing it requires a schema migration.
5. `LocationManager` does not handle `.denied` or `.restricted` authorization — the app
shows `—` for sun times without explanation.
6. `SolsticeData` coverage ends at Vintersolverv 2030. After that, all queries return nil.
|