# 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: " ", date: SolsticeData.dateFromUTC(...), 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.