diff options
| -rw-r--r-- | Solsnu.Widget/Models/Season.swift | 49 | ||||
| -rw-r--r-- | Solsnu.Widget/Models/SolsticeData.swift | 114 | ||||
| -rw-r--r-- | Solsnu.Widget/Models/SolsticeEvent.swift | 38 | ||||
| -rw-r--r-- | Solsnu.Widget/Solsnu_Widget.swift | 8 | ||||
| -rw-r--r-- | Solsnu.Widget/Solsnu_WidgetBundle.swift | 25 | ||||
| -rw-r--r-- | Solsnu.Widget/Solsnu_WidgetControl.swift | 1 | ||||
| -rw-r--r-- | Solsnu.Widget/Utilities/AppGroupManager.swift | 67 | ||||
| -rw-r--r-- | Solsnu.Widget/Utilities/SunTimes.swift | 148 | ||||
| -rw-r--r-- | Solsnu.Widget/Views/SmallWidgetView.swift | 5 | ||||
| -rw-r--r-- | Solverv.xcodeproj/project.pbxproj | 78 | ||||
| -rw-r--r-- | Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist | 2 | ||||
| -rw-r--r-- | Solverv/Models/Season.swift | 49 | ||||
| -rw-r--r-- | Solverv/Models/SolsticeData.swift | 114 | ||||
| -rw-r--r-- | Solverv/Models/SolsticeEvent.swift | 38 | ||||
| -rw-r--r-- | Solverv/SolvervApp.swift | 10 | ||||
| -rw-r--r-- | Solverv/Utilities/AppGroupManager.swift | 67 | ||||
| -rw-r--r-- | Solverv/Utilities/SunTimes.swift | 148 |
17 files changed, 39 insertions, 922 deletions
diff --git a/Solsnu.Widget/Models/Season.swift b/Solsnu.Widget/Models/Season.swift deleted file mode 100644 index 01eaf99..0000000 --- a/Solsnu.Widget/Models/Season.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -enum Season: String, Codable { - case spring - case summer - case autumn - case winter - - var displayName: String { - switch self { - case .spring: return "Spring" - case .summer: return "Summer" - case .autumn: return "Autumn" - case .winter: return "Winter" - } - } - - var description: String { - switch self { - case .spring: return "Day and night are approximately equal length" - case .summer: return "Longest day of the year" - case .autumn: return "Day and night are approximately equal length" - case .winter: return "Shortest day of the year" - } - } - - var colorLight: Color { - switch self { - case .spring: return Color(red: 0.298, green: 0.686, blue: 0.314) // #4CAF50 - case .summer: return Color(red: 1.0, green: 0.761, blue: 0.039) // #FFC107 - case .autumn: return Color(red: 1.0, green: 0.596, blue: 0.0) // #FF9800 - case .winter: return Color(red: 0.129, green: 0.588, blue: 0.953) // #2196F3 - } - } - - var assetName: String { - return "Season\(displayName)" - } - - static func fromDate(_ date: Date) -> Season { - let month = Calendar.current.component(.month, from: date) - switch month { - case 3, 4, 5: return .spring - case 6, 7, 8: return .summer - case 9, 10, 11: return .autumn - default: return .winter - } - } -} diff --git a/Solsnu.Widget/Models/SolsticeData.swift b/Solsnu.Widget/Models/SolsticeData.swift deleted file mode 100644 index 5a36da7..0000000 --- a/Solsnu.Widget/Models/SolsticeData.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation - -class SolsticeData { - static let shared = SolsticeData() - - private let events: [SolsticeEvent] - - private init() { - // Hardcoded solstice/equinox events for 2025-2030 (all in UTC) - var allEvents: [SolsticeEvent] = [] - - // 2025 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 3, day: 20, hour: 9, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 6, day: 20, hour: 14, minute: 42), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 9, day: 22, hour: 18, minute: 20), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 12, day: 21, hour: 15, minute: 3), season: .winter)) - - // 2026 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 3, day: 20, hour: 14, minute: 46), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 6, day: 21, hour: 8, minute: 25), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 9, day: 23, hour: 0, minute: 6), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 12, day: 21, hour: 20, minute: 50), season: .winter)) - - // 2027 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 3, day: 20, hour: 20, minute: 25), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 6, day: 21, hour: 14, minute: 11), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 9, day: 23, hour: 6, minute: 2), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 12, day: 22, hour: 2, minute: 43), season: .winter)) - - // 2028 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 3, day: 20, hour: 2, minute: 17), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 6, day: 20, hour: 20, minute: 2), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 9, day: 22, hour: 11, minute: 45), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 12, day: 21, hour: 8, minute: 20), season: .winter)) - - // 2029 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 3, day: 20, hour: 8, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 6, day: 21, hour: 1, minute: 48), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 9, day: 22, hour: 17, minute: 37), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 12, day: 21, hour: 14, minute: 14), season: .winter)) - - // 2030 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 3, day: 20, hour: 13, minute: 51), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 6, day: 21, hour: 7, minute: 31), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 9, day: 22, hour: 23, minute: 27), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 12, day: 21, hour: 20, minute: 9), season: .winter)) - - // Sort events by date - self.events = allEvents.sorted { $0.date < $1.date } - } - - /// Helper function to create UTC dates (static to be usable during init) - private static func dateFromUTC(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date { - var components = DateComponents() - components.year = year - components.month = month - components.day = day - components.hour = hour - components.minute = minute - components.second = 0 - components.timeZone = TimeZone(abbreviation: "UTC") - - return Calendar(identifier: .gregorian).date(from: components) ?? Date() - } - - /// Returns the next upcoming solstice/equinox event - func nextEvent() -> SolsticeEvent? { - let now = Date() - return events.first { $0.date > now } - } - - /// Returns the next N upcoming events - func upcomingEvents(count: Int) -> [SolsticeEvent] { - let now = Date() - let futureEvents = events.filter { $0.date > now } - return Array(futureEvents.prefix(count)) - } - - /// Returns the progress to the next event as (elapsed days, total days) - func progressToNextEvent() -> (elapsed: Int, total: Int)? { - guard let nextEvent = nextEvent() else { - return nil - } - - let now = Date() - let today = Calendar.current.startOfDay(for: now) - let eventDay = Calendar.current.startOfDay(for: nextEvent.date) - - // Find the previous event to calculate total days - guard let previousEventIndex = events.firstIndex(where: { $0.date > now }) else { - return nil - } - - let previousEvent: Date - if previousEventIndex > 0 { - previousEvent = events[previousEventIndex - 1].date - } else { - // If this is the first event, we need to handle this case - // Use the event itself minus some arbitrary period (not applicable for first event) - return nil - } - - let previousEventDay = Calendar.current.startOfDay(for: previousEvent) - - // Calculate elapsed and total days - let elapsedComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: today) - let totalComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: eventDay) - - let elapsed = max(0, elapsedComponents.day ?? 0) - let total = max(1, totalComponents.day ?? 1) - - return (elapsed: elapsed, total: total) - } -} diff --git a/Solsnu.Widget/Models/SolsticeEvent.swift b/Solsnu.Widget/Models/SolsticeEvent.swift deleted file mode 100644 index d8c4a7b..0000000 --- a/Solsnu.Widget/Models/SolsticeEvent.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -struct SolsticeEvent: Identifiable, Codable { - let id: UUID - let name: String - let date: Date // UTC - let season: Season - - init(name: String, date: Date, season: Season) { - self.id = UUID() - self.name = name - self.date = date - self.season = season - } - - /// Convert UTC date to user's local timezone - func localDateTime() -> Date { - let utcCalendar = Calendar(identifier: .gregorian) - let utcComponents = utcCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) - let timeZone = TimeZone.current - let offset = timeZone.secondsFromGMT(for: date) - - var localCalendar = Calendar.current - localCalendar.timeZone = timeZone - var localComponents = utcComponents - localComponents.second = (localComponents.second ?? 0) + offset - - return localCalendar.date(from: localComponents) ?? date - } - - /// Days until this event from today - func daysUntil() -> Int { - let today = Calendar.current.startOfDay(for: Date()) - let eventDay = Calendar.current.startOfDay(for: date) - let components = Calendar.current.dateComponents([.day], from: today, to: eventDay) - return max(0, components.day ?? 0) - } -} diff --git a/Solsnu.Widget/Solsnu_Widget.swift b/Solsnu.Widget/Solsnu_Widget.swift index 20e8a8e..c36cc34 100644 --- a/Solsnu.Widget/Solsnu_Widget.swift +++ b/Solsnu.Widget/Solsnu_Widget.swift @@ -171,22 +171,22 @@ extension SolvervDef { } var nextEvent: SolsticeEvent? { - SolsticeData.shared.nextEvent() + SolsticeData.shared.nextEvent(from: date) } func daysUntilNext() -> Int { guard let next = nextEvent else { return 0 } - return next.daysUntil() + return next.daysUntil(from: date) } func progressRatio() -> Double { - guard let progress = SolsticeData.shared.progressToNextEvent() else { return 0.0 } + guard let progress = SolsticeData.shared.progressToNextEvent(from: date) else { return 0.0 } let ratio = Double(progress.elapsed) / Double(progress.total) return max(0, min(1.0, ratio)) } func upcomingEventsPreview(count: Int) -> [SolsticeEvent] { - SolsticeData.shared.upcomingEvents(count: count) + SolsticeData.shared.upcomingEvents(count: count, from: date) } static var preview: SolvervDef { diff --git a/Solsnu.Widget/Solsnu_WidgetBundle.swift b/Solsnu.Widget/Solsnu_WidgetBundle.swift index 8a7b718..7bd3f1a 100644 --- a/Solsnu.Widget/Solsnu_WidgetBundle.swift +++ b/Solsnu.Widget/Solsnu_WidgetBundle.swift @@ -13,11 +13,9 @@ struct Solsnu_WidgetBundle: WidgetBundle { var body: some Widget { Solsnu_Widget() Solsnu_WidgetMedium() - Solsnu_WidgetLarge() } } -// Medium widget struct Solsnu_WidgetMedium: Widget { let kind: String = "Solsnu_Widget_Medium" @@ -34,27 +32,6 @@ struct Solsnu_WidgetMedium: Widget { } .configurationDisplayName("Solstice Countdown") .description("Days until next solstice or equinox") - .supportedFamilies([.systemMedium]) - } -} - -// Large widget -struct Solsnu_WidgetLarge: Widget { - let kind: String = "Solsnu_Widget_Large" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - if #available(iOS 17.0, *) { - LargeWidgetView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } else { - LargeWidgetView(entry: entry) - .padding() - .background() - } - } - .configurationDisplayName("Solstice Countdown") - .description("Days until next solstice or equinox") - .supportedFamilies([.systemLarge]) + .supportedFamilies([.systemMedium,.systemSmall]) } } diff --git a/Solsnu.Widget/Solsnu_WidgetControl.swift b/Solsnu.Widget/Solsnu_WidgetControl.swift index 62cd817..686a824 100644 --- a/Solsnu.Widget/Solsnu_WidgetControl.swift +++ b/Solsnu.Widget/Solsnu_WidgetControl.swift @@ -52,3 +52,4 @@ struct StartTimerIntent: SetValueIntent { return .result() } } +
\ No newline at end of file diff --git a/Solsnu.Widget/Utilities/AppGroupManager.swift b/Solsnu.Widget/Utilities/AppGroupManager.swift deleted file mode 100644 index 95ff2de..0000000 --- a/Solsnu.Widget/Utilities/AppGroupManager.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -class AppGroupManager { - static let shared = AppGroupManager() - static let appGroupID = "group.com.ivarlovlie.solverv" - - private lazy var userDefaults: UserDefaults? = { - UserDefaults(suiteName: Self.appGroupID) - }() - - // MARK: - Location Storage - - struct UserLocation: Codable { - let latitude: Double - let longitude: Double - let timestamp: String // ISO 8601 - let isDefaultLocation: Bool - } - - func saveLocation(_ location: UserLocation) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(location) { - ud.set(encoded, forKey: "userLocation") - } - } - - func getLocation() -> UserLocation? { - guard let ud = userDefaults, - let data = ud.data(forKey: "userLocation"), - let location = try? JSONDecoder().decode(UserLocation.self, from: data) else { - return nil - } - return location - } - - // MARK: - Sunrise/Sunset Storage - - struct SunTimes: Codable { - let date: String // ISO 8601 date only (YYYY-MM-DD) - let sunrise: String // ISO 8601 datetime - let sunset: String // ISO 8601 datetime - let timestamp: String // ISO 8601 when calculated - } - - func saveSunTimes(_ sunTimes: SunTimes) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(sunTimes) { - ud.set(encoded, forKey: "sunTimes") - } - } - - func getSunTimes() -> SunTimes? { - guard let ud = userDefaults, - let data = ud.data(forKey: "sunTimes"), - let sunTimes = try? JSONDecoder().decode(SunTimes.self, from: data) else { - return nil - } - return sunTimes - } - - // MARK: - Helpers - - func clearAllData() { - userDefaults?.removeObject(forKey: "userLocation") - userDefaults?.removeObject(forKey: "sunTimes") - } -} diff --git a/Solsnu.Widget/Utilities/SunTimes.swift b/Solsnu.Widget/Utilities/SunTimes.swift deleted file mode 100644 index 8c7132e..0000000 --- a/Solsnu.Widget/Utilities/SunTimes.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Foundation - -class SunTimes { - let latitude: Double - let longitude: Double - let date: Date - - init(latitude: Double, longitude: Double, date: Date) { - self.latitude = latitude - self.longitude = longitude - self.date = date - } - - /// Calculate sunrise time for the location and date - func sunrise() -> Date? { - guard let result = calculateSunTimes() else { return nil } - return result.sunrise - } - - /// Calculate sunset time for the location and date - func sunset() -> Date? { - guard let result = calculateSunTimes() else { return nil } - return result.sunset - } - - // NOAA solar position algorithm - // Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/ - private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? { - let calendar = Calendar(identifier: .gregorian) - let components = calendar.dateComponents([.year, .month, .day], from: date) - guard let year = components.year else { - return nil - } - - // Step 1: Calculate day of year - let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))! - let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1 - - // Step 2: Fractional year in radians - let daysInYear = Double(isLeapYear(year) ? 366 : 365) - let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear - - // Step 3: Solar declination (radians) - let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma) - - 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma) - - 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma) - - // Step 4: Equation of time (minutes) - let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma) - - 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma)) - - // Step 5: Hour angle at sunrise/sunset - let latRad = latitude * Double.pi / 180.0 - let cosH = -tan(latRad) * tan(decl) - - guard cosH >= -1.0 && cosH <= 1.0 else { - // Sun is always up or always down at this latitude/date - return nil - } - - let h = acos(cosH) * 180.0 / Double.pi - - // Step 6: Solar noon in UTC (decimal hours) - let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0) - - // Step 7: Sunrise and sunset in UTC (decimal hours) - let sunriseUTC = solarNoonUTC - (h / 15.0) - let sunsetUTC = solarNoonUTC + (h / 15.0) - - // Step 8: Convert to local time (add timezone offset) - let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0 - let sunriseLocal = sunriseUTC + tzOffset - let sunsetLocal = sunsetUTC + tzOffset - - // Step 9: Create date components, handling day boundary crossing - let currentCalendar = Calendar.current - let baseComponents = currentCalendar.dateComponents([.year, .month, .day], from: date) - - // Sunrise - var sunriseComp = baseComponents - let srHourDouble = sunriseLocal - let srHour = Int(srHourDouble) - let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0 - let srMinute = Int(srMinuteDouble) - - if srHour < 0 { - if let prevDay = currentCalendar.date(byAdding: .day, value: -1, to: date) { - let prevComponents = currentCalendar.dateComponents([.year, .month, .day], from: prevDay) - sunriseComp = prevComponents - sunriseComp.hour = 24 + srHour - } else { - sunriseComp.hour = 0 - } - } else if srHour >= 24 { - if let nextDay = currentCalendar.date(byAdding: .day, value: 1, to: date) { - let nextComponents = currentCalendar.dateComponents([.year, .month, .day], from: nextDay) - sunriseComp = nextComponents - sunriseComp.hour = srHour - 24 - } else { - sunriseComp.hour = 23 - } - } else { - sunriseComp.hour = srHour - } - sunriseComp.minute = max(0, min(59, srMinute)) - sunriseComp.second = 0 - - // Sunset - var sunsetComp = baseComponents - let ssHourDouble = sunsetLocal - let ssHour = Int(ssHourDouble) - let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0 - let ssMinute = Int(ssMinuteDouble) - - if ssHour < 0 { - if let prevDay = currentCalendar.date(byAdding: .day, value: -1, to: date) { - let prevComponents = currentCalendar.dateComponents([.year, .month, .day], from: prevDay) - sunsetComp = prevComponents - sunsetComp.hour = 24 + ssHour - } else { - sunsetComp.hour = 0 - } - } else if ssHour >= 24 { - if let nextDay = currentCalendar.date(byAdding: .day, value: 1, to: date) { - let nextComponents = currentCalendar.dateComponents([.year, .month, .day], from: nextDay) - sunsetComp = nextComponents - sunsetComp.hour = ssHour - 24 - } else { - sunsetComp.hour = 23 - } - } else { - sunsetComp.hour = ssHour - } - sunsetComp.minute = max(0, min(59, ssMinute)) - sunsetComp.second = 0 - - guard let sunriseDate = currentCalendar.date(from: sunriseComp), - let sunsetDate = currentCalendar.date(from: sunsetComp) else { - return nil - } - - return (sunriseDate, sunsetDate) - } - - private func isLeapYear(_ year: Int) -> Bool { - return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) - } -} diff --git a/Solsnu.Widget/Views/SmallWidgetView.swift b/Solsnu.Widget/Views/SmallWidgetView.swift index 86aa913..16ca102 100644 --- a/Solsnu.Widget/Views/SmallWidgetView.swift +++ b/Solsnu.Widget/Views/SmallWidgetView.swift @@ -10,22 +10,19 @@ struct SmallWidgetView: View { var body: some View { ZStack { Image(entry.def.bg) - .resizable() - .scaledToFill() VStack(spacing: 8) { Text("\(entry.def.daysUntilNext())") .font(.system(size: 26, weight: .bold, design: .serif)) + .position(x: 50,y: 50) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) .italic() - 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)) - } } } .containerBackground(for: .widget, alignment: .center) { Color.clear } diff --git a/Solverv.xcodeproj/project.pbxproj b/Solverv.xcodeproj/project.pbxproj index 093609f..f5e3e64 100644 --- a/Solverv.xcodeproj/project.pbxproj +++ b/Solverv.xcodeproj/project.pbxproj @@ -7,20 +7,19 @@ objects = { /* Begin PBXBuildFile section */ - A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; }; - BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; }; 0602B5968563FDCC6AAE214B /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; }; - C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; }; + 1B8629D62EF0C656005A1C75 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D52EF0C656005A1C75 /* WidgetKit.framework */; }; + 1B8629D82EF0C656005A1C75 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D72EF0C656005A1C75 /* SwiftUI.framework */; }; + 1B8629E52EF0C657005A1C75 /* Solsnu.WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 58EC7B50BC5232D031299280 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; }; - DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; }; + 594A94559E2251F892BC158B /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; }; 63374A5CD6AAFBBA4A5E87AC /* AppGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */; }; 6FFB4853DA8AF00282F20F96 /* AppGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */; }; - 594A94559E2251F892BC158B /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; }; 90AEA7599D196072837AE994 /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; }; - - 1B8629D62EF0C656005A1C75 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D52EF0C656005A1C75 /* WidgetKit.framework */; }; - 1B8629D82EF0C656005A1C75 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D72EF0C656005A1C75 /* SwiftUI.framework */; }; - 1B8629E52EF0C657005A1C75 /* Solsnu.WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; }; + BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; }; + C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; }; + DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -48,16 +47,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - BAAD0F6249013EFDE6CF3BDB /* Season.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Season.swift; sourceTree = "<group>"; }; - A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeEvent.swift; sourceTree = "<group>"; }; - 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeData.swift; sourceTree = "<group>"; }; - 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupManager.swift; sourceTree = "<group>"; }; - 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunTimes.swift; sourceTree = "<group>"; }; - 1B8629BF2EF0C636005A1C75 /* Solverv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Solverv.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Solsnu.WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 1B8629D52EF0C656005A1C75 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 1B8629D72EF0C656005A1C75 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupManager.swift; sourceTree = "<group>"; }; + 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeData.swift; sourceTree = "<group>"; }; + 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunTimes.swift; sourceTree = "<group>"; }; + A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeEvent.swift; sourceTree = "<group>"; }; + BAAD0F6249013EFDE6CF3BDB /* Season.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Season.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -106,35 +104,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0BFB6DC6E4F437012CF2990E /* Models */ = { - isa = PBXGroup; - children = ( - BAAD0F6249013EFDE6CF3BDB /* Season.swift */, - A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */, - 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */, - ); - path = Models; - sourceTree = "<group>"; - }; - 5F48ADA595F6B3DE3DFE2A32 /* Utilities */ = { - isa = PBXGroup; - children = ( - 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */, - 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */, - ); - path = Utilities; - sourceTree = "<group>"; - }; - 0EEEC5869B3AB7A2A2154C10 /* Shared */ = { - isa = PBXGroup; - children = ( - 0BFB6DC6E4F437012CF2990E /* Models */, - 5F48ADA595F6B3DE3DFE2A32 /* Utilities */, - ); - path = Shared; - sourceTree = "<group>"; - }; - 1B8629B62EF0C636005A1C75 = { isa = PBXGroup; children = ( @@ -142,6 +111,7 @@ 1B8629D92EF0C656005A1C75 /* Solsnu.Widget */, 1B8629D42EF0C656005A1C75 /* Frameworks */, 1B8629C02EF0C636005A1C75 /* Products */, + 1BC006B72FABC8FE009BB0E6 /* Recovered References */, ); sourceTree = "<group>"; }; @@ -163,6 +133,18 @@ name = Frameworks; sourceTree = "<group>"; }; + 1BC006B72FABC8FE009BB0E6 /* Recovered References */ = { + isa = PBXGroup; + children = ( + BAAD0F6249013EFDE6CF3BDB /* Season.swift */, + A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */, + 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */, + 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */, + 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */, + ); + name = "Recovered References"; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -272,24 +254,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */, + A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */, 0602B5968563FDCC6AAE214B /* SolsticeEvent.swift in Sources */, 58EC7B50BC5232D031299280 /* SolsticeData.swift in Sources */, 63374A5CD6AAFBBA4A5E87AC /* AppGroupManager.swift in Sources */, 594A94559E2251F892BC158B /* SunTimes.swift in Sources */, -); + ); runOnlyForDeploymentPostprocessing = 0; }; 1B8629CF2EF0C656005A1C75 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */, + BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */, C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */, DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */, 6FFB4853DA8AF00282F20F96 /* AppGroupManager.swift in Sources */, 90AEA7599D196072837AE994 /* SunTimes.swift in Sources */, -); + ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ diff --git a/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist b/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist index d8ecd4f..f76564c 100644 --- a/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ <key>Solsnu.WidgetExtension.xcscheme_^#shared#^_</key> <dict> <key>orderHint</key> - <integer>1</integer> + <integer>0</integer> </dict> <key>Solverv.xcscheme_^#shared#^_</key> <dict> diff --git a/Solverv/Models/Season.swift b/Solverv/Models/Season.swift deleted file mode 100644 index 01eaf99..0000000 --- a/Solverv/Models/Season.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -enum Season: String, Codable { - case spring - case summer - case autumn - case winter - - var displayName: String { - switch self { - case .spring: return "Spring" - case .summer: return "Summer" - case .autumn: return "Autumn" - case .winter: return "Winter" - } - } - - var description: String { - switch self { - case .spring: return "Day and night are approximately equal length" - case .summer: return "Longest day of the year" - case .autumn: return "Day and night are approximately equal length" - case .winter: return "Shortest day of the year" - } - } - - var colorLight: Color { - switch self { - case .spring: return Color(red: 0.298, green: 0.686, blue: 0.314) // #4CAF50 - case .summer: return Color(red: 1.0, green: 0.761, blue: 0.039) // #FFC107 - case .autumn: return Color(red: 1.0, green: 0.596, blue: 0.0) // #FF9800 - case .winter: return Color(red: 0.129, green: 0.588, blue: 0.953) // #2196F3 - } - } - - var assetName: String { - return "Season\(displayName)" - } - - static func fromDate(_ date: Date) -> Season { - let month = Calendar.current.component(.month, from: date) - switch month { - case 3, 4, 5: return .spring - case 6, 7, 8: return .summer - case 9, 10, 11: return .autumn - default: return .winter - } - } -} diff --git a/Solverv/Models/SolsticeData.swift b/Solverv/Models/SolsticeData.swift deleted file mode 100644 index 5a36da7..0000000 --- a/Solverv/Models/SolsticeData.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation - -class SolsticeData { - static let shared = SolsticeData() - - private let events: [SolsticeEvent] - - private init() { - // Hardcoded solstice/equinox events for 2025-2030 (all in UTC) - var allEvents: [SolsticeEvent] = [] - - // 2025 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 3, day: 20, hour: 9, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 6, day: 20, hour: 14, minute: 42), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 9, day: 22, hour: 18, minute: 20), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 12, day: 21, hour: 15, minute: 3), season: .winter)) - - // 2026 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 3, day: 20, hour: 14, minute: 46), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 6, day: 21, hour: 8, minute: 25), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 9, day: 23, hour: 0, minute: 6), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 12, day: 21, hour: 20, minute: 50), season: .winter)) - - // 2027 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 3, day: 20, hour: 20, minute: 25), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 6, day: 21, hour: 14, minute: 11), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 9, day: 23, hour: 6, minute: 2), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 12, day: 22, hour: 2, minute: 43), season: .winter)) - - // 2028 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 3, day: 20, hour: 2, minute: 17), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 6, day: 20, hour: 20, minute: 2), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 9, day: 22, hour: 11, minute: 45), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 12, day: 21, hour: 8, minute: 20), season: .winter)) - - // 2029 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 3, day: 20, hour: 8, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 6, day: 21, hour: 1, minute: 48), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 9, day: 22, hour: 17, minute: 37), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 12, day: 21, hour: 14, minute: 14), season: .winter)) - - // 2030 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 3, day: 20, hour: 13, minute: 51), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 6, day: 21, hour: 7, minute: 31), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 9, day: 22, hour: 23, minute: 27), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 12, day: 21, hour: 20, minute: 9), season: .winter)) - - // Sort events by date - self.events = allEvents.sorted { $0.date < $1.date } - } - - /// Helper function to create UTC dates (static to be usable during init) - private static func dateFromUTC(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date { - var components = DateComponents() - components.year = year - components.month = month - components.day = day - components.hour = hour - components.minute = minute - components.second = 0 - components.timeZone = TimeZone(abbreviation: "UTC") - - return Calendar(identifier: .gregorian).date(from: components) ?? Date() - } - - /// Returns the next upcoming solstice/equinox event - func nextEvent() -> SolsticeEvent? { - let now = Date() - return events.first { $0.date > now } - } - - /// Returns the next N upcoming events - func upcomingEvents(count: Int) -> [SolsticeEvent] { - let now = Date() - let futureEvents = events.filter { $0.date > now } - return Array(futureEvents.prefix(count)) - } - - /// Returns the progress to the next event as (elapsed days, total days) - func progressToNextEvent() -> (elapsed: Int, total: Int)? { - guard let nextEvent = nextEvent() else { - return nil - } - - let now = Date() - let today = Calendar.current.startOfDay(for: now) - let eventDay = Calendar.current.startOfDay(for: nextEvent.date) - - // Find the previous event to calculate total days - guard let previousEventIndex = events.firstIndex(where: { $0.date > now }) else { - return nil - } - - let previousEvent: Date - if previousEventIndex > 0 { - previousEvent = events[previousEventIndex - 1].date - } else { - // If this is the first event, we need to handle this case - // Use the event itself minus some arbitrary period (not applicable for first event) - return nil - } - - let previousEventDay = Calendar.current.startOfDay(for: previousEvent) - - // Calculate elapsed and total days - let elapsedComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: today) - let totalComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: eventDay) - - let elapsed = max(0, elapsedComponents.day ?? 0) - let total = max(1, totalComponents.day ?? 1) - - return (elapsed: elapsed, total: total) - } -} diff --git a/Solverv/Models/SolsticeEvent.swift b/Solverv/Models/SolsticeEvent.swift deleted file mode 100644 index d8c4a7b..0000000 --- a/Solverv/Models/SolsticeEvent.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -struct SolsticeEvent: Identifiable, Codable { - let id: UUID - let name: String - let date: Date // UTC - let season: Season - - init(name: String, date: Date, season: Season) { - self.id = UUID() - self.name = name - self.date = date - self.season = season - } - - /// Convert UTC date to user's local timezone - func localDateTime() -> Date { - let utcCalendar = Calendar(identifier: .gregorian) - let utcComponents = utcCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) - let timeZone = TimeZone.current - let offset = timeZone.secondsFromGMT(for: date) - - var localCalendar = Calendar.current - localCalendar.timeZone = timeZone - var localComponents = utcComponents - localComponents.second = (localComponents.second ?? 0) + offset - - return localCalendar.date(from: localComponents) ?? date - } - - /// Days until this event from today - func daysUntil() -> Int { - let today = Calendar.current.startOfDay(for: Date()) - let eventDay = Calendar.current.startOfDay(for: date) - let components = Calendar.current.dateComponents([.day], from: today, to: eventDay) - return max(0, components.day ?? 0) - } -} diff --git a/Solverv/SolvervApp.swift b/Solverv/SolvervApp.swift index a2beb1d..955a946 100644 --- a/Solverv/SolvervApp.swift +++ b/Solverv/SolvervApp.swift @@ -52,15 +52,6 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject { manager.requestWhenInUseAuthorization() } else if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways { fetchLocation() - } else { - // Use default location (Greenwich) - let defaultLocation = AppGroupManager.UserLocation( - latitude: 0.0, - longitude: 0.0, - timestamp: ISO8601DateFormatter().string(from: Date()), - isDefaultLocation: true - ) - AppGroupManager.shared.saveLocation(defaultLocation) } } @@ -83,6 +74,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject { timestamp: ISO8601DateFormatter().string(from: Date()), isDefaultLocation: false ) + AppGroupManager.shared.saveLocation(userLocation) // Calculate and cache sunrise/sunset diff --git a/Solverv/Utilities/AppGroupManager.swift b/Solverv/Utilities/AppGroupManager.swift deleted file mode 100644 index 95ff2de..0000000 --- a/Solverv/Utilities/AppGroupManager.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -class AppGroupManager { - static let shared = AppGroupManager() - static let appGroupID = "group.com.ivarlovlie.solverv" - - private lazy var userDefaults: UserDefaults? = { - UserDefaults(suiteName: Self.appGroupID) - }() - - // MARK: - Location Storage - - struct UserLocation: Codable { - let latitude: Double - let longitude: Double - let timestamp: String // ISO 8601 - let isDefaultLocation: Bool - } - - func saveLocation(_ location: UserLocation) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(location) { - ud.set(encoded, forKey: "userLocation") - } - } - - func getLocation() -> UserLocation? { - guard let ud = userDefaults, - let data = ud.data(forKey: "userLocation"), - let location = try? JSONDecoder().decode(UserLocation.self, from: data) else { - return nil - } - return location - } - - // MARK: - Sunrise/Sunset Storage - - struct SunTimes: Codable { - let date: String // ISO 8601 date only (YYYY-MM-DD) - let sunrise: String // ISO 8601 datetime - let sunset: String // ISO 8601 datetime - let timestamp: String // ISO 8601 when calculated - } - - func saveSunTimes(_ sunTimes: SunTimes) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(sunTimes) { - ud.set(encoded, forKey: "sunTimes") - } - } - - func getSunTimes() -> SunTimes? { - guard let ud = userDefaults, - let data = ud.data(forKey: "sunTimes"), - let sunTimes = try? JSONDecoder().decode(SunTimes.self, from: data) else { - return nil - } - return sunTimes - } - - // MARK: - Helpers - - func clearAllData() { - userDefaults?.removeObject(forKey: "userLocation") - userDefaults?.removeObject(forKey: "sunTimes") - } -} diff --git a/Solverv/Utilities/SunTimes.swift b/Solverv/Utilities/SunTimes.swift deleted file mode 100644 index 3b8f049..0000000 --- a/Solverv/Utilities/SunTimes.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Foundation - -class SunTimes { - let latitude: Double - let longitude: Double - let date: Date - - init(latitude: Double, longitude: Double, date: Date) { - self.latitude = latitude - self.longitude = longitude - self.date = date - } - - /// Calculate sunrise time for the location and date - func sunrise() -> Date? { - guard let result = calculateSunTimes() else { return nil } - return result.sunrise - } - - /// Calculate sunset time for the location and date - func sunset() -> Date? { - guard let result = calculateSunTimes() else { return nil } - return result.sunset - } - - // NOAA solar position algorithm - // Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/ - private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? { - let calendar = Calendar(identifier: .gregorian) - let components = calendar.dateComponents([.year, .month, .day], from: date) - guard let year = components.year else { - return nil - } - - // Step 1: Calculate day of year - let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))! - let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1 - - // Step 2: Fractional year in radians - let daysInYear = Double(isLeapYear(year) ? 366 : 365) - let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear - - // Step 3: Solar declination (radians) - let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma) - - 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma) - - 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma) - - // Step 4: Equation of time (minutes) - let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma) - - 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma)) - - // Step 5: Hour angle at sunrise/sunset - let latRad = latitude * Double.pi / 180.0 - let cosH = -tan(latRad) * tan(decl) - - guard cosH >= -1.0 && cosH <= 1.0 else { - // Sun is always up or always down at this latitude/date - return nil - } - - let h = acos(cosH) * 180.0 / Double.pi - - // Step 6: Solar noon in UTC (decimal hours) - let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0) - - // Step 7: Sunrise and sunset in UTC (decimal hours) - let sunriseUTC = solarNoonUTC - (h / 15.0) - let sunsetUTC = solarNoonUTC + (h / 15.0) - - // Step 8: Convert to local time (add timezone offset) - let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0 - let sunriseLocal = sunriseUTC + tzOffset - let sunsetLocal = sunsetUTC + tzOffset - - // Step 9: Create date components, handling day boundary crossing - let calendar2 = Calendar.current - let baseComponents = calendar2.dateComponents([.year, .month, .day], from: date) - - // Sunrise - var sunriseComp = baseComponents - let srHourDouble = sunriseLocal - let srHour = Int(srHourDouble) - let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0 - let srMinute = Int(srMinuteDouble) - - if srHour < 0 { - if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) { - let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay) - sunriseComp = prevComponents - sunriseComp.hour = 24 + srHour - } else { - sunriseComp.hour = 0 - } - } else if srHour >= 24 { - if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) { - let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay) - sunriseComp = nextComponents - sunriseComp.hour = srHour - 24 - } else { - sunriseComp.hour = 23 - } - } else { - sunriseComp.hour = srHour - } - sunriseComp.minute = max(0, min(59, srMinute)) - sunriseComp.second = 0 - - // Sunset - var sunsetComp = baseComponents - let ssHourDouble = sunsetLocal - let ssHour = Int(ssHourDouble) - let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0 - let ssMinute = Int(ssMinuteDouble) - - if ssHour < 0 { - if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) { - let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay) - sunsetComp = prevComponents - sunsetComp.hour = 24 + ssHour - } else { - sunsetComp.hour = 0 - } - } else if ssHour >= 24 { - if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) { - let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay) - sunsetComp = nextComponents - sunsetComp.hour = ssHour - 24 - } else { - sunsetComp.hour = 23 - } - } else { - sunsetComp.hour = ssHour - } - sunsetComp.minute = max(0, min(59, ssMinute)) - sunsetComp.second = 0 - - guard let sunriseDate = calendar2.date(from: sunriseComp), - let sunsetDate = calendar2.date(from: sunsetComp) else { - return nil - } - - return (sunriseDate, sunsetDate) - } - - private func isLeapYear(_ year: Int) -> Bool { - return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) - } -} |
