# Sunrise/Sunset Widget Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Display sunrise and sunset times in HH:mm format on small and medium widgets, calculated locally using the user's location. **Architecture:** Widget Provider fetches cached user location from AppGroupManager, calculates sunrise/sunset times using the existing SunTimes utility, and passes formatted times to widget views. Gracefully handles unavailable locations by showing no times. **Tech Stack:** - SwiftUI WidgetKit (existing) - SunTimes (NOAA algorithm, already available) - AppGroupManager (existing shared storage) --- ## File Structure **Files to Modify:** - `Solsnu.Widget/Solsnu_Widget.swift` — Add time properties to SolvervDef, update Provider logic - `Solsnu.Widget/Views/SmallWidgetView.swift` — Display sunrise/sunset if available - `Solsnu.Widget/Views/MediumWidgetView.swift` — Display sunrise/sunset if available **Files to Create:** - None (all infrastructure already exists) --- ## Task 1: Add Properties to SolvervDef **Files:** - Modify: `Solsnu.Widget/Solsnu_Widget.swift:72-90` - [ ] **Step 1: Add sunrise/sunset Date properties to SolvervDef struct** Add these properties after `bg: String`: ```swift let sunriseTime: Date? let sunsetTime: Date? ``` - [ ] **Step 2: Update existing constructors to accept nil times** Modify `init(date:Date)`: ```swift init(date: Date, sunriseTime: Date? = nil, sunsetTime: Date? = nil) { self.date = date self.sunriseTime = sunriseTime self.sunsetTime = sunsetTime self.bg = "smallbg" } ``` Modify `init(utcString:String)`: ```swift init(utcString: String, sunriseTime: Date? = nil, sunsetTime: Date? = nil) { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" formatter.timeZone = TimeZone(abbreviation: "UTC") let date = formatter.date(from: utcString)! self.date = date self.sunriseTime = sunriseTime self.sunsetTime = sunsetTime self.bg = "smallbg" } ``` - [ ] **Step 3: Add computed properties for formatted times** Add after `bg: String` property declarations: ```swift var sunriseFormatted: String { guard let time = sunriseTime else { return "" } let formatter = DateFormatter() formatter.timeStyle = .short formatter.dateStyle = .none return formatter.string(from: time) } var sunsetFormatted: String { guard let time = sunsetTime else { return "" } let formatter = DateFormatter() formatter.timeStyle = .short formatter.dateStyle = .none return formatter.string(from: time) } ``` - [ ] **Step 4: Verify compilation** Build the widget target: ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Build succeeds with no errors - [ ] **Step 5: Commit** ```bash git add Solsnu.Widget/Solsnu_Widget.swift git commit -m "feat: add sunrise/sunset properties to SolvervDef" ``` --- ## Task 2: Update Provider to Fetch and Calculate Sun Times **Files:** - Modify: `Solsnu.Widget/Solsnu_Widget.swift:11-37` (Provider struct and getTimeline method) - [ ] **Step 1: Import AppGroupManager in widget** Add to imports at top of Solsnu_Widget.swift: ```swift import WidgetKit import SwiftUI // Add this: import Combine ``` Note: Check if you need to add AppGroupManager to the widget target. It may require adding the file to the widget's Build Phases. - [ ] **Step 2: Rewrite Provider.getTimeline() to fetch location** Replace the entire `getTimeline` method with: ```swift func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { var entries: [SolvervEntry] = [] let currentDate = Date() // Fetch location from AppGroupManager let location = AppGroupManager.shared.getLocation() var sunriseTime: Date? = nil var sunsetTime: Date? = nil // If location exists and is less than 24 hours old, calculate sun times if let location = location { let locationFormatter = ISO8601DateFormatter() if let locationTimestamp = locationFormatter.date(from: location.timestamp) { let hoursSinceLocation = currentDate.timeIntervalSince(locationTimestamp) / 3600.0 if hoursSinceLocation < 24.0 { // Calculate sun times using SunTimes utility let calculator = SunTimes( latitude: location.latitude, longitude: location.longitude, date: currentDate ) sunriseTime = calculator.sunrise() sunsetTime = calculator.sunset() } } } // Create entry with calculated times let entry = SolvervEntry( def: SolvervDef( date: currentDate, sunriseTime: sunriseTime, sunsetTime: sunsetTime ) ) entries.append(entry) // Refresh at next midnight let calendar = Calendar.current var components = calendar.dateComponents([.year, .month, .day], from: currentDate) 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)! let timeline = Timeline(entries: entries, policy: .after(nextMidnight)) completion(timeline) } ``` - [ ] **Step 3: Verify Provider needs SunTimes import** At the top of Solsnu_Widget.swift, verify you have: ```swift import WidgetKit import SwiftUI ``` Note: SunTimes is in the main app target. You may need to either: - Move SunTimes to a shared framework, OR - Copy/duplicate SunTimes into the widget target, OR - Add SunTimes file to widget's Build Phases Check your project setup. If SunTimes isn't accessible to the widget, you'll need to add it to the widget's Build Phases. - [ ] **Step 4: Build and verify no errors** ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Build succeeds - [ ] **Step 5: Commit** ```bash git add Solsnu.Widget/Solsnu_Widget.swift git commit -m "feat: add location fetching and sun time calculation to widget provider" ``` --- ## Task 3: Update SmallWidgetView to Display Times **Files:** - Modify: `Solsnu.Widget/Views/SmallWidgetView.swift` - [ ] **Step 1: Understand current SmallWidgetView layout** Read the file to see how it currently displays the countdown - [ ] **Step 2: Add sunrise/sunset display** Update `body` to include times if available: ```swift var body: some View { ZStack { Image(entry.def.bg) .resizable() .scaledToFill() VStack(spacing: 8) { // Days until next event (existing) Text("\(entry.def.daysUntilNext())") .font(.system(size: 26, weight: .bold, design: .serif)) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) .italic() // Sunrise/Sunset times (new) if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty { HStack(spacing: 4) { Text("↑ \(entry.def.sunriseFormatted)") Text("↓ \(entry.def.sunsetFormatted)") } .font(.system(size: 11, weight: .regular)) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) } } .position(x: 50, y: 50) } .containerBackground(for: .widget, alignment: .center) { Color.clear } } ``` - [ ] **Step 3: Build and preview** Build the widget: ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Build succeeds Check the preview in Xcode to see the new layout with times (if available) - [ ] **Step 4: Commit** ```bash git add Solsnu.Widget/Views/SmallWidgetView.swift git commit -m "feat: display sunrise/sunset times in small widget" ``` --- ## Task 4: Update MediumWidgetView to Display Times **Files:** - Modify: `Solsnu.Widget/Views/MediumWidgetView.swift` - [ ] **Step 1: Read current MediumWidgetView** Understand its current structure - [ ] **Step 2: Add sunrise/sunset display** Add the times to the medium widget view. Example approach: ```swift var body: some View { ZStack { Image(entry.def.bg) .resizable() .scaledToFill() VStack(alignment: .leading, spacing: 12) { // Existing countdown content Text("\(entry.def.daysUntilNext())") .font(.system(size: 32, weight: .bold, design: .serif)) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) // Sunrise/Sunset times if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty { HStack { Text("Sunrise: \(entry.def.sunriseFormatted)") Spacer() Text("Sunset: \(entry.def.sunsetFormatted)") } .font(.system(size: 12, weight: .regular)) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) } } .padding() } .containerBackground(for: .widget, alignment: .topLeading) { Color.clear } } ``` (Adjust layout to match your design preferences) - [ ] **Step 3: Build and preview** ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add Solsnu.Widget/Views/MediumWidgetView.swift git commit -m "feat: display sunrise/sunset times in medium widget" ``` --- ## Task 5: Handle SunTimes Availability in Widget **Files:** - Modify: `Solsnu.Widget/Solsnu_Widget.swift` (if SunTimes is not available) **Note:** This task only applies if SunTimes cannot be imported directly into the widget. If you got build errors in Task 2 Step 3, you need to copy SunTimes into the widget target. - [ ] **Step 1: Check if SunTimes is available in widget** Try to build: ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" 2>&1 | grep -i suntimes ``` If no errors: Skip to Step 4 If error: Proceed to Step 2 - [ ] **Step 2: Copy SunTimes to widget target (if needed)** If build failed because SunTimes isn't accessible: Copy the file: ```bash cp Solverv/Utilities/SunTimes.swift Solsnu.Widget/Utilities/SunTimes.swift ``` Create the Utilities folder in widget if needed: ```bash mkdir -p Solsnu.Widget/Utilities ``` - [ ] **Step 3: Add SunTimes to widget target in Xcode** In Xcode: 1. Open the project 2. Select `Solsnu.Widget` target 3. Go to Build Phases → Compile Sources 4. Add `Solsnu.Widget/Utilities/SunTimes.swift` Or via command line: ```bash # This is typically done in Xcode UI ``` Build again: ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Build succeeds - [ ] **Step 4: Commit (if changes made)** ```bash git add Solsnu.Widget/Utilities/SunTimes.swift Solsnu.Widget/Solsnu_Widget.swift git commit -m "feat: add SunTimes calculator to widget target for sun time calculations" ``` If no changes needed, skip this commit. --- ## Task 6: Integration Testing **Files:** - Test: Solsnu_Widget preview in Xcode - [ ] **Step 1: Test with valid location** In Solsnu_Widget.swift preview, modify the preview to include sample sun times: ```swift #Preview(as: .systemSmall) { Solsnu_Widget() } timeline: { let formatter = DateFormatter() formatter.timeStyle = .short formatter.dateStyle = .none // Sample entry with sun times let springDate = Calendar.current.date(from: DateComponents(year: 2026, month: 3, day: 20))! let sunriseTime = Calendar.current.date(bySettingHour: 7, minute: 30, second: 0, of: springDate)! let sunsetTime = Calendar.current.date(bySettingHour: 19, minute: 45, second: 0, of: springDate)! SolvervEntry( def: SolvervDef( date: springDate, sunriseTime: sunriseTime, sunsetTime: sunsetTime ) ) } ``` Build the preview: ```bash xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` - [ ] **Step 2: Verify widget displays times correctly** In Xcode, view the canvas/preview and confirm: - Sunrise time displays as HH:mm format - Sunset time displays as HH:mm format - Both times appear on the widget (layout may vary) - [ ] **Step 3: Test with nil times (no location)** Update preview to test with nil times: ```swift SolvervEntry( def: SolvervDef( date: springDate, sunriseTime: nil, sunsetTime: nil ) ) ``` Build and verify: - Widget displays without crashing - No times are shown - Layout remains clean (no empty space for times) - [ ] **Step 4: Test different locations** Manually verify with sample calculations: - Oslo (59.9°N, 10.7°E): Should show reasonable spring times - Equator (0°, 0°): Should show ~6am and ~6pm - Near poles: May show nil (handled gracefully) - [ ] **Step 5: Commit testing changes (if any)** ```bash git add Solsnu.Widget/Solsnu_Widget.swift git commit -m "test: add preview entries with sunrise/sunset times" ``` --- ## Task 7: Final Integration & Verification **Files:** - Verify: All modified files - [ ] **Step 1: Run full build** ```bash xcodebuild build -scheme Solverv -destination "generic/platform=iOS" xcodebuild build -scheme Solsnu_Widget -destination "generic/platform=iOS" ``` Expected: Both schemes build successfully with no warnings - [ ] **Step 2: Check widget refresh behavior** The widget should: - Refresh at midnight (existing policy maintained) - Show sun times if location is available and fresh (< 24 hours) - Show nothing for times if location is unavailable or stale This is verified automatically via the Provider logic. - [ ] **Step 3: Verify no regressions** Check that existing functionality still works: - Countdown still displays - Solstice events still calculate correctly - Widget background image still loads - Other widget configurations still work - [ ] **Step 4: Final commit and review** Review all changes: ```bash git log --oneline -7 ``` Should see commits for: 1. Add sunrise/sunset properties to SolvervDef 2. Add location fetching and sun time calculation to widget provider 3. Display sunrise/sunset times in small widget 4. Display sunrise/sunset times in medium widget 5. (Optional) Add SunTimes to widget target 6. (Optional) Add preview entries All changes complete ✅ --- ## Notes - **AppGroupManager Dependency:** The widget must be able to access AppGroupManager to read cached location. Ensure both the main app and widget targets have access to this file or share it via a framework. - **SunTimes Availability:** The SunTimes calculator must be available to the widget. Either ensure it's in a shared framework or copy it to the widget target. - **Timezone Handling:** SunTimes already handles timezone conversion internally. Times are in device local time. - **Stale Location:** If location is older than 24 hours, no sun times are displayed. This aligns with the widget's daily refresh policy. - **Privacy:** Widget cannot request location permission—must rely on main app providing location via AppGroupManager.