diff --git a/Intents/GetSunriseTime.swift b/Intents/GetSunriseTime.swift index 4b290ecf..6217dce5 100644 --- a/Intents/GetSunriseTime.swift +++ b/Intents/GetSunriseTime.swift @@ -8,32 +8,32 @@ import Foundation import AppIntents import CoreLocation -import Solar +import SunKit struct GetSunriseTime: AppIntent { static var title: LocalizedStringResource = "Get Sunrise Time" static var description = IntentDescription("Calculate the sunrise time on a given date in a given location") - + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the sunrise time on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the sunrise for?") } - - let solar = Solar(for: date, coordinate: coordinate) - + + let sun = Sun(for: date, coordinate: coordinate) + return .result( - value: solar?.sunrise, - dialog: "\((solar?.sunrise ?? date).formatted(date: .omitted, time: .shortened))" + value: sun.sunrise, + dialog: "\((sun.sunrise ?? date).formatted(date: .omitted, time: .shortened))" ) } } diff --git a/Intents/GetSunsetTime.swift b/Intents/GetSunsetTime.swift index 069fb2a1..ee44af4b 100644 --- a/Intents/GetSunsetTime.swift +++ b/Intents/GetSunsetTime.swift @@ -8,32 +8,32 @@ import Foundation import AppIntents import CoreLocation -import Solar +import SunKit struct GetSunsetTime: AppIntent { static var title: LocalizedStringResource = "Get Sunset Time" static var description = IntentDescription("Calculate the sunset time on a given date in a given location") - + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the sunset time on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the sunset for?") } - - let solar = Solar(for: date, coordinate: coordinate) - + + let sun = Sun(for: date, coordinate: coordinate) + return .result( - value: solar?.sunset, - dialog: "\((solar?.sunset ?? date).formatted(date: .omitted, time: .shortened))" + value: sun.sunset, + dialog: "\((sun.sunset ?? date).formatted(date: .omitted, time: .shortened))" ) } } diff --git a/Intents/ViewDaylight.swift b/Intents/ViewDaylight.swift index 43c9854d..d92c473b 100644 --- a/Intents/ViewDaylight.swift +++ b/Intents/ViewDaylight.swift @@ -8,35 +8,35 @@ import Foundation import AppIntents import CoreLocation -import Solar +import SunKit struct ViewDaylight: AppIntent { static var title: LocalizedStringResource = "View Daylight" - static var description = IntentDescription("View how much daylight there is on a given day, based on the duration from that day’s sunrise to sunset.") - + static var description = IntentDescription("View how much daylight there is on a given day, based on the duration from that day's sunrise to sunset.") + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the daylight duration on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the daylight for?") } - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.hour, .minute, .second] - - let solar = Solar(for: date, coordinate: coordinate) - - let duration = (solar?.sunrise ?? .now).distance(to: solar?.sunset ?? .now) - + + let sun = Sun(for: date, coordinate: coordinate) + + let duration = sun.sunset.timeIntervalSince(sun.sunrise) + return .result( value: duration, dialog: "\(formatter.string(from: duration) ?? "") of daylight on \(date.formatted(date: .abbreviated, time: .omitted))" diff --git a/Intents/ViewRemainingDaylight.swift b/Intents/ViewRemainingDaylight.swift index f01c0d14..d9245bc3 100644 --- a/Intents/ViewRemainingDaylight.swift +++ b/Intents/ViewRemainingDaylight.swift @@ -7,47 +7,47 @@ import Foundation import AppIntents -import Solar +import SunKit import CoreLocation struct ViewRemainingDaylight: AppIntent { static var title: LocalizedStringResource = "View Remaining Daylight" static var description = IntentDescription("View how much daylight is remaining today, based on the time until sunset.") - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get today's remaining daylight in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the daylight for?") } - - let solar = Solar(coordinate: coordinate)! - + + let sun = Sun(coordinate: coordinate) + var resultValue: TimeInterval - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.hour, .minute, .second] - - if (solar.safeSunrise...solar.safeSunset).contains(.now) { - resultValue = Date().distance(to: solar.safeSunset) + + if (sun.safeSunrise...sun.safeSunset).contains(.now) { + resultValue = sun.safeSunset.timeIntervalSince(Date()) return .result( value: resultValue, dialog: "\(formatter.string(from: resultValue) ?? "") of daylight left today" ) - } else if solar.safeSunset < .now { + } else if sun.safeSunset < .now { resultValue = 0 return .result( value: resultValue, - dialog: "No daylight left today. The sun set \(formatter.string(from: solar.safeSunset.distance(to: .now)) ?? "") ago." + dialog: "No daylight left today. The sun set \(formatter.string(from: Date.now.timeIntervalSince(sun.safeSunset)) ?? "") ago." ) - } else if solar.safeSunrise > .now { - resultValue = solar.daylightDuration + } else if sun.safeSunrise > .now { + resultValue = sun.daylightDuration return .result( value: resultValue, dialog: "\(formatter.string(from: resultValue) ?? "") of daylight left today" diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 166fb41d..1be899f8 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -419,6 +419,7 @@ } }, "ceeK/Solar" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -1900,6 +1901,9 @@ } } } + }, + "Sunlight-dev/SunKit" : { + }, "Sunrise" : { "localizations" : { @@ -2176,8 +2180,12 @@ } } } + }, + "View how much daylight there is on a given day, based on the duration from that day's sunrise to sunset." : { + }, "View how much daylight there is on a given day, based on the duration from that day’s sunrise to sunset." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { diff --git a/Solstice.xcodeproj/project.pbxproj b/Solstice.xcodeproj/project.pbxproj index 29dba1a4..5c2032f7 100644 --- a/Solstice.xcodeproj/project.pbxproj +++ b/Solstice.xcodeproj/project.pbxproj @@ -16,11 +16,9 @@ 7106C88129A277460007A7EC /* CurrentLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AF28EEE48900E866CE /* CurrentLocation.swift */; }; 7106C88329A277550007A7EC /* Solstice.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7198468F28E5895F00E866CE /* Solstice.xcdatamodeld */; }; 7106C88429A2775C0007A7EC /* TimeInterval++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641BC299968BF00FE5AB5 /* TimeInterval++.swift */; }; - 7106C88529A2775C0007A7EC /* Solar++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AC28E9AA0E00E866CE /* Solar++.swift */; }; 7106C88629A2775C0007A7EC /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641BA2999631D00FE5AB5 /* Date++.swift */; }; 7106C88729A2775C0007A7EC /* View+CapsuleAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641C2299FD39E00FE5AB5 /* View+CapsuleAppearance.swift */; }; 7106C88929A277680007A7EC /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198468D28E5895F00E866CE /* Persistence.swift */; }; - 7106C88B29A277740007A7EC /* Solar in Frameworks */ = {isa = PBXBuildFile; productRef = 7106C88A29A277740007A7EC /* Solar */; }; 7106C88D29A27D0A0007A7EC /* OverviewWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7106C88C29A27D0A0007A7EC /* OverviewWidgetView.swift */; }; 710D9B712AB4752D0093A9A6 /* ContentToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 710D9B702AB4752D0093A9A6 /* ContentToggle.swift */; }; 710D9B722AB4752D0093A9A6 /* ContentToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 710D9B702AB4752D0093A9A6 /* ContentToggle.swift */; }; @@ -90,6 +88,14 @@ 715489B22F1FB8A500FD5CCE /* SolsticeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715489B02F1FB8A500FD5CCE /* SolsticeWidgetView.swift */; }; 715489B42F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715489B32F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift */; }; 715489B52F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715489B32F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift */; }; + 715DE1FB2F2A3A5700BBD7F0 /* SunKit in Frameworks */ = {isa = PBXBuildFile; productRef = 715DE1FA2F2A3A5700BBD7F0 /* SunKit */; }; + 715DE1FD2F2A3A6000BBD7F0 /* SunKit in Frameworks */ = {isa = PBXBuildFile; productRef = 715DE1FC2F2A3A6000BBD7F0 /* SunKit */; }; + 715DE1FF2F2A3A6500BBD7F0 /* SunKit in Frameworks */ = {isa = PBXBuildFile; productRef = 715DE1FE2F2A3A6500BBD7F0 /* SunKit */; }; + 715DE2012F2A3A6A00BBD7F0 /* SunKit in Frameworks */ = {isa = PBXBuildFile; productRef = 715DE2002F2A3A6A00BBD7F0 /* SunKit */; }; + 715DE2032F2A3E6900BBD7F0 /* SunKit++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */; }; + 715DE2042F2A3E6900BBD7F0 /* SunKit++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */; }; + 715DE2052F2A3E6900BBD7F0 /* SunKit++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */; }; + 715DE2062F2A3E6900BBD7F0 /* SunKit++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */; }; 716260C92E6E245A00C42756 /* TimeTravelToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716260C82E6E245A00C42756 /* TimeTravelToolbar.swift */; }; 71691B892C2E816A00E4ED96 /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = 71691B882C2E816A00E4ED96 /* RealityKitContent */; }; 71691B8B2C2E81DD00E4ED96 /* SolarSystemMiniMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71691B8A2C2E81DD00E4ED96 /* SolarSystemMiniMap.swift */; }; @@ -151,7 +157,6 @@ 7194653A2C2C1E09008408C0 /* WidgetLocationHeadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BD5E3A29A7778900E40C01 /* WidgetLocationHeadingView.swift */; }; 7194653B2C2C1E09008408C0 /* CompactLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F2C3A02ABB282D00E69426 /* CompactLabelStyle.swift */; }; 7194653C2C2C1E09008408C0 /* OverviewWidgetView+AccessoryWidgetViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719F558029DB1ABB00D7AE8E /* OverviewWidgetView+AccessoryWidgetViews.swift */; }; - 7194653D2C2C1E09008408C0 /* Solar++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AC28E9AA0E00E866CE /* Solar++.swift */; }; 7194653E2C2C1E09008408C0 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A2BDE529B747940071ACE9 /* Globals.swift */; }; 7194653F2C2C1E09008408C0 /* SolarChartWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7132AC0B29E6917F00523215 /* SolarChartWidget.swift */; }; 719465402C2C1E09008408C0 /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641BA2999631D00FE5AB5 /* Date++.swift */; }; @@ -163,7 +168,6 @@ 7194654A2C2C1E09008408C0 /* View+CapsuleAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641C2299FD39E00FE5AB5 /* View+CapsuleAppearance.swift */; }; 7194654C2C2C1E09008408C0 /* DaylightChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641B829995ED000FE5AB5 /* DaylightChart.swift */; }; 7194654D2C2C1E09008408C0 /* Solstice.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7198468F28E5895F00E866CE /* Solstice.xcdatamodeld */; }; - 7194654F2C2C1E09008408C0 /* Solar in Frameworks */ = {isa = PBXBuildFile; productRef = 7194651E2C2C1E09008408C0 /* Solar */; }; 719465502C2C1E09008408C0 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7106C86B29A276B40007A7EC /* SwiftUI.framework */; }; 719465512C2C1E09008408C0 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7106C86929A276B30007A7EC /* WidgetKit.framework */; }; 719465552C2C1E09008408C0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71A9A54F2AB82D6200C3A38C /* Localizable.xcstrings */; }; @@ -185,11 +189,9 @@ 7198468E28E5895F00E866CE /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198468D28E5895F00E866CE /* Persistence.swift */; }; 7198469128E5895F00E866CE /* Solstice.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7198468F28E5895F00E866CE /* Solstice.xcdatamodeld */; }; 7198469928E5898500E866CE /* StoreKitConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 7198469828E5898500E866CE /* StoreKitConfiguration.storekit */; }; - 7198469C28E58CAA00E866CE /* Solar in Frameworks */ = {isa = PBXBuildFile; productRef = 7198469B28E58CAA00E866CE /* Solar */; }; 7198469E28E58CCD00E866CE /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198469D28E58CCD00E866CE /* DetailView.swift */; }; 719846A828E991FA00E866CE /* LocationSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846A728E991FA00E866CE /* LocationSearchService.swift */; }; 719846AA28E992A200E866CE /* LocationSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846A928E992A200E866CE /* LocationSearchResultRow.swift */; }; - 719846AD28E9AA0E00E866CE /* Solar++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AC28E9AA0E00E866CE /* Solar++.swift */; }; 719846B028EEE48900E866CE /* CurrentLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AF28EEE48900E866CE /* CurrentLocation.swift */; }; 7198C40A29DB06A8009A457E /* CountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198C40929DB06A8009A457E /* CountdownWidget.swift */; }; 7198C40E29DB06C1009A457E /* OverviewWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198C40D29DB06C1009A457E /* OverviewWidget.swift */; }; @@ -202,7 +204,6 @@ 719F927429ACD21300C06921 /* SolsticeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198468428E5895E00E866CE /* SolsticeApp.swift */; }; 719F927529ACD21300C06921 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7198468D28E5895F00E866CE /* Persistence.swift */; }; 719F927729ACD21A00C06921 /* View+CapsuleAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641C2299FD39E00FE5AB5 /* View+CapsuleAppearance.swift */; }; - 719F927829ACD21A00C06921 /* Solar++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AC28E9AA0E00E866CE /* Solar++.swift */; }; 719F927929ACD21A00C06921 /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641BA2999631D00FE5AB5 /* Date++.swift */; }; 719F927A29ACD21A00C06921 /* AppStorage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 717F8BB829A8CAFC0015ECCB /* AppStorage++.swift */; }; 719F927B29ACD21A00C06921 /* TimeInterval++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F641BC299968BF00FE5AB5 /* TimeInterval++.swift */; }; @@ -210,7 +211,6 @@ 719F927D29ACD22100C06921 /* CurrentLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719846AF28EEE48900E866CE /* CurrentLocation.swift */; }; 719F927F29ACD22100C06921 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BD5E3F29A785FE00E40C01 /* NotificationManager.swift */; }; 719F928129ACD22100C06921 /* SkyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7117008A29A52B04001BE478 /* SkyGradient.swift */; }; - 719F928329ACD24700C06921 /* Solar in Frameworks */ = {isa = PBXBuildFile; productRef = 719F928229ACD24700C06921 /* Solar */; }; 719F928729ACD28500C06921 /* Solstice.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7198468F28E5895F00E866CE /* Solstice.xcdatamodeld */; }; 719F928829ACD29100C06921 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71DB52AD29ABD19B00D62BB7 /* ContentView.swift */; }; 71A029A92C2E838100E19819 /* EarthSceneKitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A029A82C2E838100E19819 /* EarthSceneKitView.swift */; }; @@ -405,6 +405,7 @@ 7150EB5529DC675800CA8341 /* WidgetHeadingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHeadingModifier.swift; sourceTree = ""; }; 715489B02F1FB8A500FD5CCE /* SolsticeWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeWidgetView.swift; sourceTree = ""; }; 715489B32F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeWidgetLocationManager.swift; sourceTree = ""; }; + 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SunKit++.swift"; sourceTree = ""; }; 71615FDC2F29F0610082944D /* SolsticeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SolsticeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 71615FEB2F29F06F0082944D /* SolsticeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SolsticeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 716260C82E6E245A00C42756 /* TimeTravelToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeTravelToolbar.swift; sourceTree = ""; }; @@ -444,7 +445,6 @@ 7198469D28E58CCD00E866CE /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; 719846A728E991FA00E866CE /* LocationSearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchService.swift; sourceTree = ""; }; 719846A928E992A200E866CE /* LocationSearchResultRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchResultRow.swift; sourceTree = ""; }; - 719846AC28E9AA0E00E866CE /* Solar++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Solar++.swift"; sourceTree = ""; }; 719846AF28EEE48900E866CE /* CurrentLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentLocation.swift; sourceTree = ""; }; 7198C40929DB06A8009A457E /* CountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownWidget.swift; sourceTree = ""; }; 7198C40D29DB06C1009A457E /* OverviewWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewWidget.swift; sourceTree = ""; }; @@ -505,8 +505,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 715DE1FF2F2A3A6500BBD7F0 /* SunKit in Frameworks */, 71C105AB2E7D313D00A76EBB /* TimeMachine in Frameworks */, - 7106C88B29A277740007A7EC /* Solar in Frameworks */, 7106C86C29A276B40007A7EC /* SwiftUI.framework in Frameworks */, 7106C86A29A276B40007A7EC /* WidgetKit.framework in Frameworks */, 71CB96DC2DE9AC4300660AA9 /* Suite in Frameworks */, @@ -532,7 +532,7 @@ buildActionMask = 2147483647; files = ( 71C105A92E7D313900A76EBB /* TimeMachine in Frameworks */, - 7194654F2C2C1E09008408C0 /* Solar in Frameworks */, + 715DE2012F2A3A6A00BBD7F0 /* SunKit in Frameworks */, 719465502C2C1E09008408C0 /* SwiftUI.framework in Frameworks */, 719465512C2C1E09008408C0 /* WidgetKit.framework in Frameworks */, 71CB96DE2DE9AC4700660AA9 /* Suite in Frameworks */, @@ -543,9 +543,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 715DE1FB2F2A3A5700BBD7F0 /* SunKit in Frameworks */, 71CB96D82DE9AC3600660AA9 /* Suite in Frameworks */, 71691B892C2E816A00E4ED96 /* RealityKitContent in Frameworks */, - 7198469C28E58CAA00E866CE /* Solar in Frameworks */, 71C105962E7D2F0800A76EBB /* TimeMachine in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -554,9 +554,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 719F928329ACD24700C06921 /* Solar in Frameworks */, 71C105982E7D2F1200A76EBB /* TimeMachine in Frameworks */, 71CB96DA2DE9AC3E00660AA9 /* Suite in Frameworks */, + 715DE1FD2F2A3A6000BBD7F0 /* SunKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -787,7 +787,7 @@ 71F641BA2999631D00FE5AB5 /* Date++.swift */, 712433922D77A4CE00709C20 /* EdgeInsets++.swift */, 712433AA2D79090100709C20 /* NSImage+pngData.swift */, - 719846AC28E9AA0E00E866CE /* Solar++.swift */, + 715DE2022F2A3E6900BBD7F0 /* SunKit++.swift */, 71F641BC299968BF00FE5AB5 /* TimeInterval++.swift */, 71C105A32E7D311700A76EBB /* TimeMachine++.swift */, 7195125729B48ECD009D282F /* TimeZone++.swift */, @@ -908,9 +908,9 @@ ); name = "Widget Extension"; packageProductDependencies = ( - 7106C88A29A277740007A7EC /* Solar */, 71CB96DB2DE9AC4300660AA9 /* Suite */, 71C105AA2E7D313D00A76EBB /* TimeMachine */, + 715DE1FE2F2A3A6500BBD7F0 /* SunKit */, ); productName = WidgetExtension; productReference = 7106C86829A276B30007A7EC /* Widget Extension.appex */; @@ -976,9 +976,9 @@ ); name = "watchOS Widget Extension"; packageProductDependencies = ( - 7194651E2C2C1E09008408C0 /* Solar */, 71CB96DD2DE9AC4700660AA9 /* Suite */, 71C105A82E7D313900A76EBB /* TimeMachine */, + 715DE2002F2A3A6A00BBD7F0 /* SunKit */, ); productName = WidgetExtension; productReference = 7194655C2C2C1E09008408C0 /* watchOS Widget Extension.appex */; @@ -1002,10 +1002,10 @@ ); name = Solstice; packageProductDependencies = ( - 7198469B28E58CAA00E866CE /* Solar */, 71691B882C2E816A00E4ED96 /* RealityKitContent */, 71CB96D72DE9AC3600660AA9 /* Suite */, 71C105952E7D2F0800A76EBB /* TimeMachine */, + 715DE1FA2F2A3A5700BBD7F0 /* SunKit */, ); productName = Solstice; productReference = 7198468128E5895E00E866CE /* Solstice.app */; @@ -1027,9 +1027,9 @@ ); name = "Solstice watchOS Watch App"; packageProductDependencies = ( - 719F928229ACD24700C06921 /* Solar */, 71CB96D92DE9AC3E00660AA9 /* Suite */, 71C105972E7D2F1200A76EBB /* TimeMachine */, + 715DE1FC2F2A3A6000BBD7F0 /* SunKit */, ); productName = "Solstice watchOS Watch App"; productReference = 719F924329ACD1CC00C06921 /* Solstice watchOS Watch App.app */; @@ -1078,9 +1078,9 @@ ); mainGroup = 7198467828E5895E00E866CE; packageReferences = ( - 7198469A28E58CAA00E866CE /* XCRemoteSwiftPackageReference "Solar" */, 71CB96D62DE9AC3600660AA9 /* XCRemoteSwiftPackageReference "Suite" */, 71C105942E7D2F0800A76EBB /* XCRemoteSwiftPackageReference "TimeMachine" */, + 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */, ); productRefGroup = 7198468228E5895E00E866CE /* Products */; projectDirPath = ""; @@ -1179,6 +1179,7 @@ 7150CC5C29AB7C3200E6B90C /* SolsticeCalculator.swift in Sources */, 7121DE0229C22E7D0031EEE7 /* View+Debugging.swift in Sources */, 7198C40E29DB06C1009A457E /* OverviewWidget.swift in Sources */, + 715DE2052F2A3E6900BBD7F0 /* SunKit++.swift in Sources */, 719465172C294B42008408C0 /* Color+Mix.swift in Sources */, 715489B52F201D8400FD5CCE /* SolsticeWidgetLocationManager.swift in Sources */, 719F557D29DB1A4300D7AE8E /* CountdownWidgetView+AccessoryWidgetViews.swift in Sources */, @@ -1202,7 +1203,6 @@ 71F2C3A12ABB282D00E69426 /* CompactLabelStyle.swift in Sources */, 712433962D77A4CE00709C20 /* EdgeInsets++.swift in Sources */, 719F558129DB1ABC00D7AE8E /* OverviewWidgetView+AccessoryWidgetViews.swift in Sources */, - 7106C88529A2775C0007A7EC /* Solar++.swift in Sources */, 71A2BDE829B747940071ACE9 /* Globals.swift in Sources */, 7132AC0C29E6917F00523215 /* SolarChartWidget.swift in Sources */, 71E3C4182EA12B030005C884 /* View+backportGlassEffect.swift in Sources */, @@ -1268,13 +1268,13 @@ 71FBEE6C2E4B345700EC1EF0 /* Bundle++.swift in Sources */, 719465392C2C1E09008408C0 /* WidgetHeadingModifier.swift in Sources */, 7194653A2C2C1E09008408C0 /* WidgetLocationHeadingView.swift in Sources */, + 715DE2062F2A3E6900BBD7F0 /* SunKit++.swift in Sources */, 712433AE2D79090500709C20 /* NSImage+pngData.swift in Sources */, 7194653B2C2C1E09008408C0 /* CompactLabelStyle.swift in Sources */, 71E3C41A2EA12B030005C884 /* View+backportGlassEffect.swift in Sources */, 71E3C4152EA12ABE0005C884 /* CircleWithSlice.swift in Sources */, 712433942D77A4CE00709C20 /* EdgeInsets++.swift in Sources */, 7194653C2C2C1E09008408C0 /* OverviewWidgetView+AccessoryWidgetViews.swift in Sources */, - 7194653D2C2C1E09008408C0 /* Solar++.swift in Sources */, 7194653E2C2C1E09008408C0 /* Globals.swift in Sources */, 7194653F2C2C1E09008408C0 /* SolarChartWidget.swift in Sources */, 719465402C2C1E09008408C0 /* Date++.swift in Sources */, @@ -1334,6 +1334,7 @@ 7116B1EA2DDD07F70018A294 /* View+timeMachineOverlayModifier.swift in Sources */, 71F641C3299FD39E00FE5AB5 /* View+CapsuleAppearance.swift in Sources */, 7132AC1929E6962D00523215 /* ConditionalGlobals.swift in Sources */, + 715DE2042F2A3E6900BBD7F0 /* SunKit++.swift in Sources */, 711782D82E4DFDFE0006CE5B /* DeduplicateLocationRecordsModifier.swift in Sources */, 712433A92D78870600709C20 /* StackedButtonStyle.swift in Sources */, 71BC51CB2E91B56D0025892F /* CircularSolarChart.swift in Sources */, @@ -1351,7 +1352,6 @@ 719846AA28E992A200E866CE /* LocationSearchResultRow.swift in Sources */, 71A029B42C368E1900E19819 /* SavedLocation++.swift in Sources */, 718B136C29A918680001D4DC /* SupporterSettings.swift in Sources */, - 719846AD28E9AA0E00E866CE /* Solar++.swift in Sources */, 71908ACD2AC95A5500C7B610 /* StringBuilder.swift in Sources */, 712433932D77A4CE00709C20 /* EdgeInsets++.swift in Sources */, 712433A62D78867300709C20 /* StackedLabelStyle.swift in Sources */, @@ -1386,7 +1386,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 719F927829ACD21A00C06921 /* Solar++.swift in Sources */, 71C105902E7C08DA00A76EBB /* AlignedIconLabelStyle.swift in Sources */, 719F928129ACD22100C06921 /* SkyGradient.swift in Sources */, 71789AA62C32DCC7002B8A33 /* View+MaterialListRowBackground.swift in Sources */, @@ -1427,6 +1426,7 @@ 719465162C294B42008408C0 /* Color+Mix.swift in Sources */, 71E3C41B2EA12B030005C884 /* View+backportGlassEffect.swift in Sources */, 7194650F2C22B61E008408C0 /* AppChangeMigrator.swift in Sources */, + 715DE2032F2A3E6900BBD7F0 /* SunKit++.swift in Sources */, 7121DE0129C22E7D0031EEE7 /* View+Debugging.swift in Sources */, 719F927D29ACD22100C06921 /* CurrentLocation.swift in Sources */, 71FBEE6B2E4B345700EC1EF0 /* Bundle++.swift in Sources */, @@ -2112,20 +2112,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 7194651F2C2C1E09008408C0 /* XCRemoteSwiftPackageReference "Solar" */ = { + 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ceeK/Solar"; + repositoryURL = "https://github.com/SunKit-Swift/SunKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 3.0.0; - }; - }; - 7198469A28E58CAA00E866CE /* XCRemoteSwiftPackageReference "Solar" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ceeK/Solar"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.0.0; + minimumVersion = 2.8.1; }; }; 71C105942E7D2F0800A76EBB /* XCRemoteSwiftPackageReference "TimeMachine" */ = { @@ -2147,29 +2139,29 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7106C88A29A277740007A7EC /* Solar */ = { + 715DE1FA2F2A3A5700BBD7F0 /* SunKit */ = { isa = XCSwiftPackageProductDependency; - package = 7198469A28E58CAA00E866CE /* XCRemoteSwiftPackageReference "Solar" */; - productName = Solar; + package = 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */; + productName = SunKit; }; - 71691B882C2E816A00E4ED96 /* RealityKitContent */ = { + 715DE1FC2F2A3A6000BBD7F0 /* SunKit */ = { isa = XCSwiftPackageProductDependency; - productName = RealityKitContent; + package = 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */; + productName = SunKit; }; - 7194651E2C2C1E09008408C0 /* Solar */ = { + 715DE1FE2F2A3A6500BBD7F0 /* SunKit */ = { isa = XCSwiftPackageProductDependency; - package = 7194651F2C2C1E09008408C0 /* XCRemoteSwiftPackageReference "Solar" */; - productName = Solar; + package = 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */; + productName = SunKit; }; - 7198469B28E58CAA00E866CE /* Solar */ = { + 715DE2002F2A3A6A00BBD7F0 /* SunKit */ = { isa = XCSwiftPackageProductDependency; - package = 7198469A28E58CAA00E866CE /* XCRemoteSwiftPackageReference "Solar" */; - productName = Solar; + package = 715DE1F92F2A3A5700BBD7F0 /* XCRemoteSwiftPackageReference "SunKit" */; + productName = SunKit; }; - 719F928229ACD24700C06921 /* Solar */ = { + 71691B882C2E816A00E4ED96 /* RealityKitContent */ = { isa = XCSwiftPackageProductDependency; - package = 7198469A28E58CAA00E866CE /* XCRemoteSwiftPackageReference "Solar" */; - productName = Solar; + productName = RealityKitContent; }; 71C105952E7D2F0800A76EBB /* TimeMachine */ = { isa = XCSwiftPackageProductDependency; diff --git a/Solstice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Solstice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 828d38d9..3f51a721 100644 --- a/Solstice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Solstice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "solar", + "identity" : "suite", "kind" : "remoteSourceControl", - "location" : "https://github.com/ceeK/Solar", + "location" : "https://github.com/daneden/Suite", "state" : { - "revision" : "c2b96f2d5fb7f835b91cefac5e83101f54643901", - "version" : "3.0.1" + "revision" : "e9aea55d107c4185103243a1f389d96e12e239e1", + "version" : "1.0.0" } }, { - "identity" : "suite", + "identity" : "sunkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/daneden/Suite", + "location" : "https://github.com/SunKit-Swift/SunKit", "state" : { - "revision" : "e9aea55d107c4185103243a1f389d96e12e239e1", - "version" : "1.0.0" + "revision" : "086d345cd9886b037ae0af0db49fcaf849acb887", + "version" : "2.8.1" } }, { diff --git a/Solstice/Charts/AnnualDaylightChart.swift b/Solstice/Charts/AnnualDaylightChart.swift index 99098646..234e2102 100644 --- a/Solstice/Charts/AnnualDaylightChart.swift +++ b/Solstice/Charts/AnnualDaylightChart.swift @@ -6,15 +6,15 @@ // import SwiftUI -import Solar +import SunKit import Charts import TimeMachine struct AnnualDaylightChart: View { @Environment(\.timeMachine) var timeMachine: TimeMachine var location: Location - - var kvPairs: KeyValuePairs = [ + + var kvPairs: KeyValuePairs = [ .astronomical: .indigo, .nautical: .blue, .civil: .teal, @@ -39,75 +39,72 @@ struct AnnualDaylightChart: View { } private var monthlyBarMarks: some ChartContent { - ForEach(monthlySolars, id: \.date) { solar in - solarBarMarks(for: solar) + ForEach(monthlySolars, id: \.date) { sun in + solarBarMarks(for: sun) } } @ChartContentBuilder - private func solarBarMarks(for solar: Solar) -> some ChartContent { - astronomicalBarMark(for: solar) - nauticalBarMark(for: solar) - civilBarMark(for: solar) - daylightBarMark(for: solar) + private func solarBarMarks(for sun: Sun) -> some ChartContent { + astronomicalBarMark(for: sun) + nauticalBarMark(for: sun) + civilBarMark(for: sun) + daylightBarMark(for: sun) } @ChartContentBuilder - private func astronomicalBarMark(for solar: Solar) -> some ChartContent { - if let astronomicalSunrise = solar.astronomicalSunrise?.withTimeZoneAdjustment(for: location.timeZone), - let astronomicalSunset = solar.astronomicalSunset?.withTimeZoneAdjustment(for: location.timeZone) { - let yStart: Double = max(0, solar.startOfDay.distance(to: astronomicalSunrise)) - let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: astronomicalSunset)) - BarMark( - x: .value("Astronomical Twilight", solar.date, unit: .month), - yStart: .value("Astronomical Sunrise", yStart), - yEnd: .value("Astronomical Sunset", yEnd) - ) - .foregroundStyle(by: .value("Phase", Solar.Phase.astronomical)) - } + private func astronomicalBarMark(for sun: Sun) -> some ChartContent { + let astronomicalSunrise = sun.astronomicalSunrise.withTimeZoneAdjustment(for: location.timeZone) + let astronomicalSunset = sun.astronomicalSunset.withTimeZoneAdjustment(for: location.timeZone) + let yStart: Double = max(0, astronomicalSunrise.timeIntervalSince(sun.startOfDay)) + let yEnd: Double = min(dayLength, astronomicalSunset.timeIntervalSince(sun.startOfDay)) + BarMark( + x: .value("Astronomical Twilight", sun.date, unit: .month), + yStart: .value("Astronomical Sunrise", yStart), + yEnd: .value("Astronomical Sunset", yEnd) + ) + .foregroundStyle(by: .value("Phase", Sun.Phase.astronomical)) } @ChartContentBuilder - private func nauticalBarMark(for solar: Solar) -> some ChartContent { - if let nauticalSunrise = solar.nauticalSunrise?.withTimeZoneAdjustment(for: location.timeZone), - let nauticalSunset = solar.nauticalSunset?.withTimeZoneAdjustment(for: location.timeZone) { - let yStart: Double = max(0, solar.startOfDay.distance(to: nauticalSunrise)) - let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: nauticalSunset)) - BarMark( - x: .value("Nautical Twilight", solar.date, unit: .month), - yStart: .value("Nautical Sunrise", yStart), - yEnd: .value("Nautical Sunset", yEnd) - ) - .foregroundStyle(by: .value("Phase", Solar.Phase.nautical)) - } + private func nauticalBarMark(for sun: Sun) -> some ChartContent { + let nauticalSunrise = sun.nauticalSunrise.withTimeZoneAdjustment(for: location.timeZone) + let nauticalSunset = sun.nauticalSunset.withTimeZoneAdjustment(for: location.timeZone) + let yStart: Double = max(0, nauticalSunrise.timeIntervalSince(sun.startOfDay)) + let yEnd: Double = min(dayLength, nauticalSunset.timeIntervalSince(sun.startOfDay)) + BarMark( + x: .value("Nautical Twilight", sun.date, unit: .month), + yStart: .value("Nautical Sunrise", yStart), + yEnd: .value("Nautical Sunset", yEnd) + ) + .foregroundStyle(by: .value("Phase", Sun.Phase.nautical)) } @ChartContentBuilder - private func civilBarMark(for solar: Solar) -> some ChartContent { - if let civilSunrise = solar.civilSunrise?.withTimeZoneAdjustment(for: location.timeZone), - let civilSunset = solar.civilSunset?.withTimeZoneAdjustment(for: location.timeZone) { - let yStart: Double = max(0, solar.startOfDay.distance(to: civilSunrise)) - let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: civilSunset)) - BarMark( - x: .value("Civil Twilight", solar.date, unit: .month), - yStart: .value("Civil Sunrise", yStart), - yEnd: .value("Civil Sunset", yEnd) - ) - .foregroundStyle(by: .value("Phase", Solar.Phase.civil)) - } + private func civilBarMark(for sun: Sun) -> some ChartContent { + let civilSunrise = sun.civilSunrise.withTimeZoneAdjustment(for: location.timeZone) + let civilSunset = sun.civilSunset.withTimeZoneAdjustment(for: location.timeZone) + let yStart: Double = max(0, civilSunrise.timeIntervalSince(sun.startOfDay)) + let yEnd: Double = min(dayLength, civilSunset.timeIntervalSince(sun.startOfDay)) + BarMark( + x: .value("Civil Twilight", sun.date, unit: .month), + yStart: .value("Civil Sunrise", yStart), + yEnd: .value("Civil Sunset", yEnd) + ) + .foregroundStyle(by: .value("Phase", Sun.Phase.civil)) } - private func daylightBarMark(for solar: Solar) -> some ChartContent { - let sunrise: Date = solar.safeSunrise.withTimeZoneAdjustment(for: location.timeZone) - let sunset: Date = solar.safeSunset.withTimeZoneAdjustment(for: location.timeZone) - let yStart: Double = max(0, solar.startOfDay.distance(to: sunrise)) - let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: sunset)) + private func daylightBarMark(for sun: Sun) -> some ChartContent { + let sunrise: Date = sun.safeSunrise.withTimeZoneAdjustment(for: location.timeZone) + let sunset: Date = sun.safeSunset.withTimeZoneAdjustment(for: location.timeZone) + let yStart: Double = max(0, sunrise.timeIntervalSince(sun.startOfDay)) + let yEnd: Double = min(dayLength, sunset.timeIntervalSince(sun.startOfDay)) return BarMark( - x: .value("Daylight", solar.date, unit: .month), + x: .value("Daylight", sun.date, unit: .month), yStart: .value("Sunrise", yStart), yEnd: .value("Sunset", yEnd) ) - .foregroundStyle(by: .value("Phase", Solar.Phase.day)) + .foregroundStyle(by: .value("Phase", Sun.Phase.day)) } private var yAxisMarks: some AxisContent { @@ -138,23 +135,23 @@ struct AnnualDaylightChart: View { } extension AnnualDaylightChart { - var monthlySolars: Array { + var monthlySolars: Array { guard let year = calendar.dateInterval(of: .year, for: timeMachine.date) else { return [] } - + var lastDate = calendar.date(bySetting: .day, value: 21, of: year.start) ?? year.start lastDate = calendar.date(bySetting: .hour, value: 12, of: lastDate) ?? lastDate var dates: Array = [] - + while lastDate < year.end { dates.append(lastDate) lastDate = calendar.date(byAdding: .month, value: 1, to: lastDate) ?? lastDate.addingTimeInterval(60 * 60 * 24 * 7 * 4) } - + return dates.map { date in - return Solar(for: date, coordinate: location.coordinate) - }.compactMap { $0 } + return Sun(for: date, coordinate: location.coordinate) + } } } diff --git a/Solstice/Charts/CircularSolarChart.swift b/Solstice/Charts/CircularSolarChart.swift index 2299e3f5..b497c6a9 100644 --- a/Solstice/Charts/CircularSolarChart.swift +++ b/Solstice/Charts/CircularSolarChart.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import TimeMachine import Suite import enum Accelerate.vDSP @@ -18,16 +18,20 @@ struct CircularSolarChart: View { #if WIDGET_EXTENSION @Environment(\.widgetRenderingMode) private var widgetRenderingMode #endif - + @State private var size = CGSize.zero + @State private var cachedSun: Sun? + @State private var lastLocationKey: String? var date: Date? - + var location: Location - + var timeZone: TimeZone { location.timeZone } - - var solar: Solar? { - Solar(for: date ?? timeMachine.date, coordinate: location.coordinate) + + var sun: Sun? { cachedSun } + + private var locationKey: String { + "\(location.coordinate.latitude),\(location.coordinate.longitude)" } var majorSunSize: Double { @@ -81,7 +85,7 @@ struct CircularSolarChart: View { return calendar } - func angle(for date: Date) -> Angle { + func angle(for date: Date) -> SwiftUI.Angle { Helpers.angle(for: date, timeZone: timeZone) } @@ -90,7 +94,7 @@ struct CircularSolarChart: View { #if WIDGET_EXTENSION if widgetRenderingMode == .fullColor { if appearance == .graphical { - solar?.view + sun?.view } else { Rectangle().fill(.regularMaterial) } @@ -99,7 +103,7 @@ struct CircularSolarChart: View { } #else if appearance == .graphical { - solar?.view + sun?.view } else { Rectangle().fill(.regularMaterial) } @@ -112,7 +116,7 @@ struct CircularSolarChart: View { .frame(width: majorSunSize) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.trailing, minorSunSize / 2) - .rotationEffect(angle(for: solar?.date ?? .now)) + .rotationEffect(angle(for: sun?.date ?? .now)) .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -128,12 +132,12 @@ struct CircularSolarChart: View { .frame(width: majorSunSize) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.trailing, minorSunSize / 2) - .rotationEffect(angle(for: solar?.date ?? .now)) + .rotationEffect(angle(for: sun?.date ?? .now)) .frame(maxWidth: .infinity, maxHeight: .infinity) } var safeSunriseSunsetShape: some Shape { - CircleWithSlice(startAngle: angle(for: solar?.sunrise ?? .now).degrees, endAngle: angle(for: solar?.sunset ?? .now).degrees) + CircleWithSlice(startAngle: angle(for: sun?.sunrise ?? .now).degrees, endAngle: angle(for: sun?.sunset ?? .now).degrees) } var sundial: some View { @@ -175,8 +179,8 @@ struct CircularSolarChart: View { private var phaseSlices: some View { Group { - if let phases = solar?.phases { - let phaseKeys: [Solar.Phase] = Array(phases.keys) + if let phases = sun?.phases { + let phaseKeys: [Sun.Phase] = Array(phases.keys) ForEach(phaseKeys, id: \.self) { key in phaseSlice(for: key, phases: phases) } @@ -186,7 +190,7 @@ struct CircularSolarChart: View { } @ViewBuilder - private func phaseSlice(for key: Solar.Phase, phases: [Solar.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { + private func phaseSlice(for key: Sun.Phase, phases: [Sun.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { if let (sunrise, sunset) = phases[key], let sunrise, let sunset { @@ -257,8 +261,8 @@ struct CircularSolarChart: View { @ViewBuilder private var phaseMarkers: some View { - if let phases = solar?.phases { - let phaseKeys: [Solar.Phase] = Array(phases.keys) + if let phases = sun?.phases { + let phaseKeys: [Sun.Phase] = Array(phases.keys) ForEach(phaseKeys, id: \.self) { key in phaseMarker(for: key, phases: phases) } @@ -266,7 +270,7 @@ struct CircularSolarChart: View { } @ViewBuilder - private func phaseMarker(for key: Solar.Phase, phases: [Solar.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { + private func phaseMarker(for key: Sun.Phase, phases: [Sun.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { if let (sunrise, sunset) = phases[key], let sunrise, let sunset { @@ -288,18 +292,18 @@ struct CircularSolarChart: View { @ViewBuilder var labels: some View { - if let solar { - ChartLabel(text: Text(solar.safeSunrise, style: .time), + if let sun { + ChartLabel(text: Text(sun.safeSunrise, style: .time), imageName: "sunrise", - angle: angle(for: solar.safeSunrise)) - - ChartLabel(text: Text(solar.safeSunset, style: .time), + angle: angle(for: sun.safeSunrise)) + + ChartLabel(text: Text(sun.safeSunset, style: .time), imageName: "sunset", - angle: angle(for: solar.safeSunset)) + angle: angle(for: sun.safeSunset)) } - if let duration = solar?.daylightDuration, - let diff = solar?.compactDifferenceString { + if let duration = sun?.daylightDuration, + let diff = sun?.compactDifferenceString { VStack(spacing: 2) { HStack(spacing: 2) { Image(systemName: "hourglass") @@ -352,13 +356,26 @@ struct CircularSolarChart: View { .monospacedDigit() .frame(maxWidth: .infinity) .aspectRatio(1, contentMode: .fit) + .onChange(of: date ?? timeMachine.date) { _, newDate in + cachedSun?.setDate(newDate) + } + .onChange(of: locationKey) { _, _ in + cachedSun = Sun(for: date ?? timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) + lastLocationKey = locationKey + } + .onAppear { + if cachedSun == nil || lastLocationKey != locationKey { + cachedSun = Sun(for: date ?? timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) + lastLocationKey = locationKey + } + } } } fileprivate struct ChartLabel: View { var text: Text var imageName: String - var angle: Angle + var angle: SwiftUI.Angle var body: some View { HStack(spacing: 2) { @@ -376,7 +393,7 @@ fileprivate struct ChartLabel: View { } fileprivate struct Helpers { - static func angle(for date: Date, timeZone: TimeZone = .current) -> Angle { + static func angle(for date: Date, timeZone: TimeZone = .current) -> SwiftUI.Angle { var calendar = Calendar.current calendar.timeZone = timeZone let components = calendar.dateComponents([.hour, .minute, .second], from: date) diff --git a/Solstice/Charts/DaylightChart.swift b/Solstice/Charts/DaylightChart.swift index d2308fe4..37daeefe 100644 --- a/Solstice/Charts/DaylightChart.swift +++ b/Solstice/Charts/DaylightChart.swift @@ -7,17 +7,17 @@ import SwiftUI import Charts -import Solar +import SunKit import Suite struct DaylightChart: View { @Environment(\.isLuminanceReduced) var isLuminanceReduced @Environment(\.colorScheme) var colorScheme - @State private var selectedEvent: Solar.Event? + @State private var selectedEvent: Sun.Event? @State private var currentX: Date? - var solar: Solar + var sun: Sun var timeZone: TimeZone var showEventTypes = true @@ -29,7 +29,7 @@ struct DaylightChart: View { var yScale = -1.5...1.5 var plotDate: Date { - currentX ?? solar.date + currentX ?? sun.date } var markForegroundColor: Color { @@ -52,15 +52,15 @@ struct DaylightChart: View { var calendar = Calendar.current calendar.timeZone = timeZone - let startOfDay = calendar.startOfDay(for: solar.date) - let endOfDay = max(solar.safeSunset, calendar.date(byAdding: DateComponents(day: 1), to: startOfDay) ?? solar.date) + let startOfDay = calendar.startOfDay(for: sun.date) + let endOfDay = max(sun.safeSunset, calendar.date(byAdding: DateComponents(day: 1), to: startOfDay) ?? sun.date) return startOfDay...endOfDay } var body: some View { VStack(alignment: .leading) { if includesSummaryTitle { - DaylightSummaryTitle(solar: solar, event: selectedEvent, date: currentX, timeZone: timeZone) + DaylightSummaryTitle(sun: sun, event: selectedEvent, date: currentX, timeZone: timeZone) } chartContent @@ -77,7 +77,7 @@ struct DaylightChart: View { .environment(\.colorScheme, .dark) } .environment(\.timeZone, timeZone) - .preference(key: DaylightGradientTimePreferenceKey.self, value: currentX ?? solar.date) + .preference(key: DaylightGradientTimePreferenceKey.self, value: currentX ?? sun.date) } private var chartContent: some View { @@ -109,11 +109,11 @@ struct DaylightChart: View { } } - private var filteredEvents: [Solar.Event] { - solar.events.filter { range.contains($0.date) } + private var filteredEvents: [Sun.Event] { + sun.events.filter { range.contains($0.date) } } - private func eventPointMark(for solarEvent: Solar.Event) -> some ChartContent { + private func eventPointMark(for solarEvent: Sun.Event) -> some ChartContent { PointMark( x: .value("Event Time", solarEvent.date), y: .value("Event", yValue(for: solarEvent.date)) @@ -123,15 +123,15 @@ struct DaylightChart: View { .symbolSize(markSize * .pi * 2) } - private func eventPointOpacity(for phase: Solar.Phase) -> Double { - let hiddenPhases: Set = [.night, .day, .sunrise, .sunset] - let shouldShow: Bool = showEventTypes || !Solar.Phase.plottablePhases.contains(phase) + private func eventPointOpacity(for phase: Sun.Phase) -> Double { + let hiddenPhases: Set = [.night, .day, .sunrise, .sunset] + let shouldShow: Bool = showEventTypes || !Sun.Phase.plottablePhases.contains(phase) return (shouldShow && !hiddenPhases.contains(phase)) ? 1 : 0 } @ViewBuilder private func chartOverlayContent(proxy: ChartProxy, geo: GeometryProxy) -> some View { - let horizonY: CGFloat = proxy.position(forY: yValue(for: solar.safeSunrise)) ?? 0 + let horizonY: CGFloat = proxy.position(forY: yValue(for: sun.safeSunrise)) ?? 0 Group { horizonLine(width: geo.size.width, yOffset: horizonY) @@ -286,7 +286,7 @@ extension DaylightChart { var relativeEventTimeString: String { if let selectedEvent, calendar.isDateInToday(selectedEvent.date) { - return " (\((selectedEvent.date.. HierarchicalShapeStyle { + func pointMarkColor(for eventPhase: Sun.Phase) -> HierarchicalShapeStyle { switch eventPhase { case .astronomical: return .quaternary @@ -321,7 +321,7 @@ extension DaylightChart { } func resetSelectedEvent() { - selectedEvent = solar.events.filter { + selectedEvent = sun.events.filter { $0.phase == .sunset || $0.phase == .sunrise }.sorted(by: { a, b in a.date.compare(.now) == .orderedDescending @@ -329,7 +329,7 @@ extension DaylightChart { } func progressValue(for date: Date) -> Double { - return (date.distance(to: startOfDay) - culminationDelta) / dayLength + return (startOfDay.timeIntervalSince(date) - culminationDelta) / dayLength } func yValue(for date: Date) -> Double { @@ -348,8 +348,8 @@ extension DaylightChart { currentX = proxy.value(atX: xCurrent) if let currentX, - let nearestEvent = solar.events.sorted(by: { lhs, rhs in - abs(lhs.date.distance(to: currentX)) <= abs(rhs.date.distance(to: currentX)) + let nearestEvent = sun.events.sorted(by: { lhs, rhs in + abs(currentX.timeIntervalSince(lhs.date)) <= abs(currentX.timeIntervalSince(rhs.date)) }).first { selectedEvent = nearestEvent } @@ -383,13 +383,13 @@ extension DaylightChart { Form { Group { DaylightChart( - solar: Solar(coordinate: TemporaryLocation.placeholderLondon.coordinate)!, + sun: Sun(coordinate: TemporaryLocation.placeholderLondon.coordinate), timeZone: TimeZone.autoupdatingCurrent, scrubbable: true ) DaylightChart( - solar: Solar(coordinate: TemporaryLocation.placeholderLondon.coordinate)!, + sun: Sun(coordinate: TemporaryLocation.placeholderLondon.coordinate), timeZone: TimeZone.autoupdatingCurrent, appearance: .graphical, scrubbable: true diff --git a/Solstice/ContentView.swift b/Solstice/ContentView.swift index 60b6a19b..f7e27aa1 100644 --- a/Solstice/ContentView.swift +++ b/Solstice/ContentView.swift @@ -7,7 +7,7 @@ import SwiftUI import CoreData -import Solar +import SunKit import Suite import TimeMachine diff --git a/Solstice/Detail View/AnnualOverview.swift b/Solstice/Detail View/AnnualOverview.swift index 5a354278..a15ad595 100644 --- a/Solstice/Detail View/AnnualOverview.swift +++ b/Solstice/Detail View/AnnualOverview.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import Suite import TimeMachine @@ -22,9 +22,11 @@ struct AnnualOverview: View { @Environment(\.openWindow) var openWindow #endif @Environment(\.timeMachine) var timeMachine: TimeMachine - + @State private var isInformationSheetPresented = false - + @State private var cachedDecemberSolsticeSun: Sun? + @State private var cachedJuneSolsticeSun: Sun? + var location: Location var nextGreaterThanPrevious: Bool { @@ -169,38 +171,43 @@ struct AnnualOverview: View { } } .materialListRowBackground() + .task(id: solsticeDependencies) { + updateSolsticeSuns() + } } } extension AnnualOverview { - var decemberSolsticeSolar: Solar? { - let year = calendar.component(.year, from: timeMachine.date) - let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) - return Solar(for: decemberSolstice, coordinate: location.coordinate) - } - - var juneSolsticeSolar: Solar? { - let year = calendar.component(.year, from: timeMachine.date) - let juneSolstice = SolsticeCalculator.juneSolstice(year: year) - return Solar(for: juneSolstice, coordinate: location.coordinate) - } - - var longestDay: Solar? { - guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + var decemberSolsticeSun: Sun? { cachedDecemberSolsticeSun } + var juneSolsticeSun: Sun? { cachedJuneSolsticeSun } + + var longestDay: Sun? { + guard let decemberSolsticeSun, + let juneSolsticeSun else { return nil } - - return decemberSolsticeSolar.daylightDuration > juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar + return decemberSolsticeSun.daylightDuration > juneSolsticeSun.daylightDuration ? decemberSolsticeSun : juneSolsticeSun } - - var shortestDay: Solar? { - guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + + var shortestDay: Sun? { + guard let decemberSolsticeSun, + let juneSolsticeSun else { return nil } - - return decemberSolsticeSolar.daylightDuration < juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar + return decemberSolsticeSun.daylightDuration < juneSolsticeSun.daylightDuration ? decemberSolsticeSun : juneSolsticeSun + } + + var solsticeDependencies: [AnyHashable] { + let year = calendar.component(.year, from: timeMachine.date) + return [year, location.coordinate.latitude, location.coordinate.longitude] + } + + func updateSolsticeSuns() { + let year = calendar.component(.year, from: timeMachine.date) + let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) + let juneSolstice = SolsticeCalculator.juneSolstice(year: year) + cachedDecemberSolsticeSun = Sun(for: decemberSolstice, coordinate: location.coordinate, timeZone: location.timeZone) + cachedJuneSolsticeSun = Sun(for: juneSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } } diff --git a/Solstice/Detail View/DailyOverview.swift b/Solstice/Detail View/DailyOverview.swift index 29b1e241..c2bc03ff 100644 --- a/Solstice/Detail View/DailyOverview.swift +++ b/Solstice/Detail View/DailyOverview.swift @@ -6,34 +6,43 @@ // import SwiftUI -import Solar +import SunKit import Suite import TimeMachine struct DailyOverview: View { @Environment(\.timeMachine) private var timeMachine - var solar: Solar + var sun: Sun var location: Location - - @State private var gradientSolar: Solar? - + + @State private var gradientSun: Sun? + @State private var cachedDifferenceFromPreviousSolstice: TimeInterval = 0 + @State private var lastDifferenceKey: String? + @AppStorage(Preferences.detailViewChartAppearance) private var chartAppearance @AppStorage(Preferences.chartType) private var chartType - + var solarDateIsInToday: Bool { var calendar = Calendar.autoupdatingCurrent calendar.timeZone = location.timeZone - return calendar.isDate(solar.date, inSameDayAs: Date()) + return calendar.isDate(sun.date, inSameDayAs: Date()) } - - var differenceFromPreviousSolstice: TimeInterval? { - guard let solar = Solar(for: timeMachine.date, coordinate: location.coordinate), - let previousSolsticeSolar = Solar(for: solar.date.previousSolstice, coordinate: location.coordinate) else { - return nil - } - - return previousSolsticeSolar.daylightDuration - solar.daylightDuration + + var differenceFromPreviousSolstice: TimeInterval { + cachedDifferenceFromPreviousSolstice + } + + private var differenceKey: String { + let year = calendar.component(.year, from: timeMachine.date) + return "\(year),\(location.coordinate.latitude),\(location.coordinate.longitude)" + } + + private func updateDifferenceFromPreviousSolstice() { + let currentSun = Sun(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) + let previousSolsticeSun = Sun(for: currentSun.date.previousSolstice, coordinate: location.coordinate, timeZone: location.timeZone) + cachedDifferenceFromPreviousSolstice = previousSolsticeSun.daylightDuration - currentSun.daylightDuration + lastDifferenceKey = differenceKey } var nextGreaterThanPrevious: Bool { @@ -88,7 +97,7 @@ struct DailyOverview: View { .alignmentGuide(.listRowSeparatorTrailing) { d in d[.trailing] } #if !os(visionOS) .listRowBackground( - solar.view + sun.view .opacity(chartType == .circular && chartAppearance == .graphical ? 0.3 : 0) .mask { LinearGradient(colors: [.black, .clear], startPoint: .top, endPoint: .bottom) @@ -104,7 +113,7 @@ struct DailyOverview: View { Group { Label { AdaptiveStack { - Text(Duration.seconds(solar.daylightDuration).formatted(.units(maximumUnitCount: 2))) + Text(Duration.seconds(sun.daylightDuration).formatted(.units(maximumUnitCount: 2))) } label: { Text("Total daylight") } @@ -112,10 +121,10 @@ struct DailyOverview: View { Image(systemName: "hourglass") } - if solarDateIsInToday && (solar.safeSunrise...solar.safeSunset).contains(solar.date) { + if solarDateIsInToday && (sun.safeSunrise...sun.safeSunset).contains(sun.date) { Label { AdaptiveStack { - Text(timerInterval: solar.safeSunrise...solar.safeSunset) + Text(timerInterval: sun.safeSunrise...sun.safeSunset) .monospacedDigit() } label: { Text("Remaining daylight") @@ -127,39 +136,27 @@ struct DailyOverview: View { Label { AdaptiveStack { - if let sunrise = solar.sunrise { - Text(sunrise, style: .time) - } else { - Text("—") - } + Text(sun.sunrise, style: .time) } label: { Text("Sunrise") } } icon: { Image(systemName: "sunrise") } - + Label { AdaptiveStack { - if let solarNoon = solar.solarNoon { - Text(solarNoon, style: .time) - } else { - Text("—") - } + Text(sun.solarNoon, style: .time) } label: { Text("Solar noon") } } icon: { Image(systemName: "sun.max") } - + Label { AdaptiveStack { - if let sunset = solar.sunset { - Text(sunset, style: .time) - } else { - Text("—") - } + Text(sun.sunset, style: .time) } label: { Text("Sunset") } @@ -175,18 +172,24 @@ struct DailyOverview: View { HStack { Text("Local time") Spacer() - Text("\(solar.date, style: .time) (\(location.timeZone.differenceStringFromLocalTime(for: timeMachine.date)))") + Text("\(sun.date, style: .time) (\(location.timeZone.differenceStringFromLocalTime(for: timeMachine.date)))") } .environment(\.timeZone, location.timeZone) } } footer: { - if let differenceFromPreviousSolstice { - Label { - Text("\(Duration.seconds(abs(differenceFromPreviousSolstice)).formatted(.units(maximumUnitCount: 2))) \(nextGreaterThanPrevious ? "more" : "less") daylight \(timeMachine.dateLabel(context: .middleOfSentence)) compared to the previous solstice") - } icon: { - Image(systemName: nextGreaterThanPrevious ? "chart.line.uptrend.xyaxis" : "chart.line.downtrend.xyaxis") - .contentTransition(.symbolEffect) - } + Label { + Text("\(Duration.seconds(abs(differenceFromPreviousSolstice)).formatted(.units(maximumUnitCount: 2))) \(nextGreaterThanPrevious ? "more" : "less") daylight \(timeMachine.dateLabel(context: .middleOfSentence)) compared to the previous solstice") + } icon: { + Image(systemName: nextGreaterThanPrevious ? "chart.line.uptrend.xyaxis" : "chart.line.downtrend.xyaxis") + .contentTransition(.symbolEffect) + } + } + .onChange(of: differenceKey) { _, _ in + updateDifferenceFromPreviousSolstice() + } + .onAppear { + if lastDifferenceKey != differenceKey { + updateDifferenceFromPreviousSolstice() } } } @@ -196,7 +199,7 @@ extension DailyOverview { @ViewBuilder var daylightChartView: some View { DaylightChart( - solar: solar, + sun: sun, timeZone: location.timeZone, appearance: chartAppearance, scrubbable: true, markSize: chartMarkSize @@ -208,11 +211,15 @@ extension DailyOverview { .if(chartAppearance == .graphical) { content in content .background { - SkyGradient(solar: gradientSolar ?? solar) + SkyGradient(sun: gradientSun ?? sun) } } .onPreferenceChange(DaylightGradientTimePreferenceKey.self) { date in - self.gradientSolar = Solar(for: date, coordinate: solar.coordinate) + if gradientSun != nil { + gradientSun?.setDate(date) + } else { + gradientSun = Sun(for: date, coordinate: sun.coordinate, timeZone: location.timeZone) + } } #endif #if os(macOS) @@ -223,7 +230,7 @@ extension DailyOverview { #Preview { Form { - DailyOverview(solar: Solar(coordinate: TemporaryLocation.placeholderLondon.coordinate)!, location: TemporaryLocation.placeholderLondon) + DailyOverview(sun: Sun(coordinate: TemporaryLocation.placeholderLondon.coordinate), location: TemporaryLocation.placeholderLondon) } .withTimeMachine(.solsticeTimeMachine) } diff --git a/Solstice/Detail View/DaylightSummaryTitle.swift b/Solstice/Detail View/DaylightSummaryTitle.swift index b9ddafe0..8a05f48b 100644 --- a/Solstice/Detail View/DaylightSummaryTitle.swift +++ b/Solstice/Detail View/DaylightSummaryTitle.swift @@ -6,11 +6,11 @@ // import SwiftUI -import Solar +import SunKit struct DaylightSummaryTitle: View { - var solar: Solar - var event: Solar.Event? + var sun: Sun + var event: Sun.Event? var date: Date? var timeZone = localTimeZone @@ -37,7 +37,7 @@ struct DaylightSummaryTitle: View { var body: some View { VStack(alignment: .leading) { HStack { - Text(solar.differenceString) + Text(sun.differenceString) .contentTransition(.numericText()) .font(summaryFont) .fontWeight(.semibold) @@ -66,12 +66,12 @@ struct DaylightSummaryTitle: View { .foregroundStyle(.secondary) Group { - if date < solar.safeSunrise { - Text("Sunrise in \((date.. solar.safeSunset && date <= (solar.tomorrow?.safeSunrise ?? solar.endOfDay) { - Text("Sunrise in \((date..<(solar.tomorrow?.safeSunrise ?? solar.endOfDay)).formatted(formatter))") + if date < sun.safeSunrise { + Text("Sunrise in \((date.. sun.safeSunset && date <= sun.tomorrow.safeSunrise { + Text("Sunrise in \((date..: View { #endif @State private var showRemainingDaylight = false @State private var showShareSheet = false - + @State private var cachedSun: Sun? + @State private var lastLocationKey: String? + @AppStorage(Preferences.detailViewChartAppearance) private var chartAppearance @SceneStorage("selectedLocation") private var selectedLocation: String? - - var solar: Solar? { - Solar(for: timeMachine.date, coordinate: location.coordinate) + + var sun: Sun? { cachedSun } + + private var locationKey: String { + "\(location.coordinate.latitude),\(location.coordinate.longitude)" } var navBarTitleText: Text { @@ -43,8 +47,8 @@ struct DetailView: View { var body: some View { Form { - if let solar { - DailyOverview(solar: solar, location: location) + if let sun { + DailyOverview(sun: sun, location: location) } AnnualOverview(location: location) @@ -71,9 +75,9 @@ struct DetailView: View { } #if os(watchOS) .modify { - if let solar { + if let sun { $0.containerBackground( - SkyGradient(solar: solar), + SkyGradient(sun: sun), for: .navigation ) } else { @@ -82,8 +86,21 @@ struct DetailView: View { } #endif .sheet(isPresented: $showShareSheet) { - if let solar { - ShareSolarChartView(solar: solar, location: location, chartAppearance: chartAppearance) + if let sun { + ShareSolarChartView(sun: sun, location: location, chartAppearance: chartAppearance) + } + } + .onChange(of: timeMachine.date) { _, newDate in + cachedSun?.setDate(newDate) + } + .onChange(of: locationKey) { _, _ in + cachedSun = Sun(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) + lastLocationKey = locationKey + } + .onAppear { + if cachedSun == nil || lastLocationKey != locationKey { + cachedSun = Sun(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) + lastLocationKey = locationKey } } } diff --git a/Solstice/Detail View/SolarExtremetiesOverview.swift b/Solstice/Detail View/SolarExtremetiesOverview.swift index f87c953d..38044630 100644 --- a/Solstice/Detail View/SolarExtremetiesOverview.swift +++ b/Solstice/Detail View/SolarExtremetiesOverview.swift @@ -6,19 +6,27 @@ // import SwiftUI -import Solar +import SunKit import TimeMachine struct SolarExtremetiesOverview: View { @Environment(\.timeMachine) private var timeMachine - + + @State private var cachedDecemberSolsticeSun: Sun? + @State private var cachedJuneSolsticeSun: Sun? + var location: Location - + var body: some View { - if let shortestDay, - let longestDay { - SolarExtremityView(solar: longestDay, extremity: .longest) - SolarExtremityView(solar: shortestDay, extremity: .shortest) + Group { + if let shortestDay, + let longestDay { + SolarExtremityView(sun: longestDay, extremity: .longest) + SolarExtremityView(sun: shortestDay, extremity: .shortest) + } + } + .task(id: solsticeDependencies) { + updateSolsticeSuns() } } } @@ -48,19 +56,19 @@ fileprivate struct SolarExtremityView: View { } } - var solar: Solar + var sun: Sun var extremity: Extremity var body: some View { CompatibleDisclosureGroup { Button("Time travel to date", systemImage: "clock.arrow.2.circlepath") { withAnimation { - timeMachine.date = solar.date + timeMachine.date = sun.date } } - - let duration = Duration.seconds(solar.daylightDuration).formatted(.units(maximumUnitCount: 2)) - + + let duration = Duration.seconds(sun.daylightDuration).formatted(.units(maximumUnitCount: 2)) + Label { AdaptiveStack { Text(duration) @@ -70,34 +78,30 @@ fileprivate struct SolarExtremityView: View { } icon: { Image(systemName: "hourglass") } - - if let sunrise = solar.sunrise { - Label { - AdaptiveStack { - Text(sunrise, style: .time) - } label: { - Text("Sunrise") - } - } icon: { - Image(systemName: "sunrise") + + Label { + AdaptiveStack { + Text(sun.sunrise, style: .time) + } label: { + Text("Sunrise") } + } icon: { + Image(systemName: "sunrise") } - - if let sunset = solar.sunset { - Label { - AdaptiveStack { - Text(sunset, style: .time) - } label: { - Text("Sunset") - } - } icon: { - Image(systemName: "sunset") + + Label { + AdaptiveStack { + Text(sun.sunset, style: .time) + } label: { + Text("Sunset") } + } icon: { + Image(systemName: "sunset") } } label: { Label { AdaptiveStack { - Text(solar.date, style: .date) + Text(sun.date, style: .date) } label: { Text(extremity.title) } @@ -157,34 +161,36 @@ extension CompatibleDisclosureGroup { } extension SolarExtremetiesOverview { - var decemberSolsticeSolar: Solar? { - let year = calendar.component(.year, from: timeMachine.date) - let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) - return Solar(for: decemberSolstice, coordinate: location.coordinate) - } - - var juneSolsticeSolar: Solar? { - let year = calendar.component(.year, from: timeMachine.date) - let juneSolstice = SolsticeCalculator.juneSolstice(year: year) - return Solar(for: juneSolstice, coordinate: location.coordinate) - } - - var longestDay: Solar? { - guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + var decemberSolsticeSun: Sun? { cachedDecemberSolsticeSun } + var juneSolsticeSun: Sun? { cachedJuneSolsticeSun } + + var longestDay: Sun? { + guard let decemberSolsticeSun, + let juneSolsticeSun else { return nil } - - return decemberSolsticeSolar.daylightDuration > juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar + return decemberSolsticeSun.daylightDuration > juneSolsticeSun.daylightDuration ? decemberSolsticeSun : juneSolsticeSun } - - var shortestDay: Solar? { - guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + + var shortestDay: Sun? { + guard let decemberSolsticeSun, + let juneSolsticeSun else { return nil } - - return decemberSolsticeSolar.daylightDuration < juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar + return decemberSolsticeSun.daylightDuration < juneSolsticeSun.daylightDuration ? decemberSolsticeSun : juneSolsticeSun + } + + var solsticeDependencies: [AnyHashable] { + let year = calendar.component(.year, from: timeMachine.date) + return [year, location.coordinate.latitude, location.coordinate.longitude] + } + + func updateSolsticeSuns() { + let year = calendar.component(.year, from: timeMachine.date) + let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) + let juneSolstice = SolsticeCalculator.juneSolstice(year: year) + cachedDecemberSolsticeSun = Sun(for: decemberSolstice, coordinate: location.coordinate, timeZone: location.timeZone) + cachedJuneSolsticeSun = Sun(for: juneSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } } diff --git a/Solstice/Extensions/AppStorage++.swift b/Solstice/Extensions/AppStorage++.swift index f6f6feb8..51a7c71f 100644 --- a/Solstice/Extensions/AppStorage++.swift +++ b/Solstice/Extensions/AppStorage++.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit fileprivate let defaultNotificationDate = calendar.date(bySettingHour: 8, minute: 0, second: 0, of: Date()) ?? .now @@ -95,7 +95,7 @@ struct Preferences { static let defaultDateComponents = DateComponents(timeZone: .autoupdatingCurrent, hour: 8, minute: 0) /// Which solar event notifications are sent relative to - static let relation: Value = ("notificationRelation", .sunrise) + static let relation: Value = ("notificationRelation", .sunrise) /// The offset in seconds between the notification and the chosen solar event static let relativeOffset: Value = ("notificationRelativeOffset", 30 * 60) diff --git a/Solstice/Extensions/Solar++.swift b/Solstice/Extensions/Solar++.swift deleted file mode 100644 index bd9cfddc..00000000 --- a/Solstice/Extensions/Solar++.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// Solar++.swift -// Solstice -// -// Created by Daniel Eden on 02/10/2022. -// - -import SwiftUI -import Solar -import Charts - -// MARK: - Solar Event Types -extension Solar { - struct Event: Hashable, Identifiable { - var id: Int { hashValue } - let label: String - var date: Date - let phase: Phase - - init?(label: String, date: Date?, phase: Phase) { - guard let date else { return nil } - self.label = label - self.date = date - self.phase = phase - } - - var imageName: String { - switch phase { - case .sunrise: - return "sunrise" - case .sunset: - return "sunset" - default: - return "sun.max" - } - } - - var description: String { - phase.rawValue - } - } - - enum Phase: String, Plottable, CaseIterable { - case night = "Night", - astronomical = "Astronomical Twilight", - nautical = "Nautical Twilight", - civil = "Civil Twilight", - day = "Day", - sunrise = "Sunrise", - sunset = "Sunset" - - static let plottablePhases: [Phase] = [.astronomical, .nautical, .civil] - } - - var phases: [Phase: (sunrise: Date?, sunset: Date?)] { - [ - .astronomical: (astronomicalSunrise, astronomicalSunset), - .nautical: (nauticalSunrise, nauticalSunset), - .civil: (civilSunrise, civilSunset), - .day: (safeSunrise, safeSunset) - ] - } -} - -// MARK: - Day Boundaries -extension Solar { - var startOfDay: Date { - calendar.startOfDay(for: date) - } - - var endOfDay: Date { - var components = DateComponents() - components.day = 1 - guard let endOfDay = calendar.date(byAdding: components, to: startOfDay) else { return date } - return endOfDay - } - - private var culmination: Date { - let halfDistance = abs(safeSunset.distance(to: safeSunrise)) / 2 - return safeSunrise.addingTimeInterval(halfDistance) - } -} - -// MARK: - Safe Sunrise/Sunset -extension Solar { - var fallbackSunrise: Date? { - sunrise ?? civilSunrise ?? nauticalSunrise ?? astronomicalSunrise - } - - var fallbackSunset: Date? { - sunset ?? civilSunset ?? nauticalSunset ?? astronomicalSunset - } - - var safeSunrise: Date { - if let sunrise { - return sunrise - } - - if daylightDuration <= 0 { - return startOfDay.addingTimeInterval(.twentyFourHours / 2) - } else if daylightDuration >= .twentyFourHours { - return endOfDay - } - - return fallbackSunrise ?? startOfDay - } - - var safeSunset: Date { - if let sunset { - return sunset - } - - if daylightDuration <= 0 { - return startOfDay.addingTimeInterval(.twentyFourHours / 2) - } else if daylightDuration >= .twentyFourHours { - return endOfDay - } - - return fallbackSunset ?? endOfDay - } - - var daylightDuration: TimeInterval { - if let sunrise, let sunset { - return sunrise.distance(to: sunset) - } - - guard coordinate.insidePolarCircle else { - return safeSunrise.distance(to: safeSunset) - } - - let month = Calendar.current.component(.month, from: date) - - switch month { - case 1...3, 10...12: - return coordinate.insideArcticCircle ? 0 : TimeInterval.twentyFourHours - default: - return coordinate.insideArcticCircle ? TimeInterval.twentyFourHours : 0 - } - } - - var solarNoon: Date? { - guard let fallbackSunrise, let fallbackSunset else { return nil } - return fallbackSunrise.addingTimeInterval(fallbackSunrise.distance(to: fallbackSunset) / 2) - } -} - -// MARK: - Related Days -extension Solar { - var yesterday: Solar? { - let yesterdayDate = calendar.date(byAdding: .day, value: -1, to: date) ?? date.addingTimeInterval(60 * 60 * 24 - 1) - return Solar(for: yesterdayDate, coordinate: coordinate) - } - - var tomorrow: Solar? { - guard let tomorrow = calendar.date(byAdding: .day, value: 1, to: date) else { - return nil - } - - return Solar(for: tomorrow, coordinate: coordinate) - } -} - -// MARK: - Difference Strings -extension Solar { - var compactDifferenceString: LocalizedStringKey { - let comparator = date.isToday ? yesterday : Solar(coordinate: self.coordinate) - let difference = daylightDuration - (comparator?.daylightDuration ?? 0) - let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) - - let moreOrLess = difference >= 0 ? "+" : "-" - - return LocalizedStringKey("\(moreOrLess)\(differenceString)") - } - - var differenceString: LocalizedStringKey { - let formatter = DateFormatter() - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .medium - formatter.formattingContext = .middleOfSentence - - let comparator = date.isToday ? yesterday : Solar(coordinate: self.coordinate) - let difference = daylightDuration - (comparator?.daylightDuration ?? 0) - let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) - - let moreOrLess = difference >= 0 ? NSLocalizedString("more", comment: "More daylight middle of sentence") : NSLocalizedString("less", comment: "Less daylight middle of sentence") - - // Check if the base date formatted as a string contains numbers. - // If it does, this means it's presented as an absolute date, and should - // be rendered as "on {date}"; if not, it's presented as a relative date, - // and should be presented as "{yesterday/today/tomorrow}" - var baseDateString = formatter.string(from: date) - if baseDateString.contains(/\d/) { - baseDateString = NSLocalizedString("on \(baseDateString)", comment: "Sentence fragment for nominal date") - } - - let comparatorDate = comparator?.date ?? self.date - - return LocalizedStringKey("\(differenceString) \(moreOrLess) daylight \(baseDateString) compared to \(formatter.string(from: comparatorDate))") - } -} - -// MARK: - Events -extension Solar { - var events: Array { - [ - Event(label: "Astronomical Sunrise", date: astronomicalSunrise, phase: .astronomical), - Event(label: "Nautical Sunrise", date: nauticalSunrise, phase: .nautical), - Event(label: "Civil Sunrise", date: civilSunrise, phase: .civil), - Event(label: "Sunrise", date: safeSunrise, phase: .sunrise), - Event(label: "Solar noon", date: culmination, phase: .day), - Event(label: "Sunset", date: safeSunset, phase: .sunset), - Event(label: "Civil Sunset", date: civilSunset, phase: .civil), - Event(label: "Nautical Sunset", date: nauticalSunset, phase: .nautical), - Event(label: "Astronomical Sunset", date: astronomicalSunset, phase: .astronomical), - ] - .compactMap { $0 } - .sorted { a, b in - a.date.compare(b.date) == .orderedAscending - } - } - - var nextSolarEvent: Event? { - let sunriseOrSunsetEvents: [Event] = events.filter { $0.phase == .sunset || $0.phase == .sunrise } - let todayEvent: Event? = sunriseOrSunsetEvents.first(where: { $0.date > date }) - if let todayEvent { - return todayEvent - } - let tomorrowEvents: [Event] = tomorrow?.events.filter { $0.phase == .sunset || $0.phase == .sunrise } ?? [] - return tomorrowEvents.first(where: { $0.date > date }) - } - - var previousSolarEvent: Event? { - let sunriseOrSunsetEvents: [Event] = events.filter { $0.phase == .sunset || $0.phase == .sunrise } - let todayEvent: Event? = sunriseOrSunsetEvents.last(where: { $0.date < date }) - if let todayEvent { - return todayEvent - } - let fallbackSolar: Solar = yesterday ?? self - let fallbackEvents: [Event] = fallbackSolar.events.filter { $0.phase == .sunset || $0.phase == .sunrise } - return fallbackEvents.last(where: { $0.date < date }) - } -} diff --git a/Solstice/Extensions/SunKit++.swift b/Solstice/Extensions/SunKit++.swift new file mode 100644 index 00000000..e6343e48 --- /dev/null +++ b/Solstice/Extensions/SunKit++.swift @@ -0,0 +1,324 @@ +// +// SunKit++.swift +// Solstice +// +// Created by Daniel Eden on 02/10/2022. +// Migrated from Solar++ to SunKit +// + +import SwiftUI +import SunKit +import Charts +import CoreLocation + +// MARK: - Solar Event Types +extension Sun { + struct Event: Hashable, Identifiable { + var id: Int { hashValue } + let label: String + var date: Date + let phase: Phase + + init(label: String, date: Date, phase: Phase) { + self.label = label + self.date = date + self.phase = phase + } + + var imageName: String { + switch phase { + case .sunrise: + return "sunrise" + case .sunset: + return "sunset" + default: + return "sun.max" + } + } + + var description: String { + phase.rawValue + } + } + + enum Phase: String, Plottable, CaseIterable { + case night = "Night", + astronomical = "Astronomical Twilight", + nautical = "Nautical Twilight", + civil = "Civil Twilight", + day = "Day", + sunrise = "Sunrise", + sunset = "Sunset" + + static let plottablePhases: [Phase] = [.astronomical, .nautical, .civil] + } + + var phases: [Phase: (sunrise: Date, sunset: Date)] { + [ + .astronomical: (astronomicalDawn, astronomicalDusk), + .nautical: (nauticalDawn, nauticalDusk), + .civil: (civilDawn, civilDusk), + .day: (safeSunrise, safeSunset) + ] + } +} + +// MARK: - Compatibility Properties (Solar naming conventions) +extension Sun { + /// Maps SunKit's astronomicalDawn to Solar's astronomicalSunrise + var astronomicalSunrise: Date { astronomicalDawn } + + /// Maps SunKit's astronomicalDusk to Solar's astronomicalSunset + var astronomicalSunset: Date { astronomicalDusk } + + /// Maps SunKit's nauticalDawn to Solar's nauticalSunrise + var nauticalSunrise: Date { nauticalDawn } + + /// Maps SunKit's nauticalDusk to Solar's nauticalSunset + var nauticalSunset: Date { nauticalDusk } + + /// Maps SunKit's civilDawn to Solar's civilSunrise + var civilSunrise: Date { civilDawn } + + /// Maps SunKit's civilDusk to Solar's civilSunset + var civilSunset: Date { civilDusk } + + /// The coordinate used for calculations + var coordinate: CLLocationCoordinate2D { + location.coordinate + } + + /// Calendar for date calculations + var calendar: Calendar { + var calendar = Calendar.current + calendar.timeZone = timeZone + return calendar + } +} + +// MARK: - Day Boundaries +extension Sun { + var startOfDay: Date { + calendar.startOfDay(for: date) + } + + var endOfDay: Date { + var components = DateComponents() + components.day = 1 + guard let endOfDay = calendar.date(byAdding: components, to: startOfDay) else { return date } + return endOfDay + } + + private var culmination: Date { + let halfDistance = abs(safeSunrise.timeIntervalSince(safeSunset)) / 2 + return safeSunrise.addingTimeInterval(halfDistance) + } +} + +// MARK: - Safe Sunrise/Sunset +extension Sun { + var fallbackSunrise: Date { + // SunKit always returns dates, but for polar edge cases we cascade through twilight + if isCircumPolar { + if isAlwaysDay { + return startOfDay + } else if isAlwaysNight { + return startOfDay.addingTimeInterval(.twentyFourHours / 2) + } + } + return sunrise + } + + var fallbackSunset: Date { + if isCircumPolar { + if isAlwaysDay { + return endOfDay + } else if isAlwaysNight { + return startOfDay.addingTimeInterval(.twentyFourHours / 2) + } + } + return sunset + } + + var safeSunrise: Date { + if isCircumPolar { + if isAlwaysNight { + // Polar night - return midday as a placeholder + return startOfDay.addingTimeInterval(.twentyFourHours / 2) + } else if isAlwaysDay { + // Midnight sun - sunrise is effectively start of day + return startOfDay + } + } + return sunrise + } + + var safeSunset: Date { + if isCircumPolar { + if isAlwaysNight { + // Polar night - return midday as a placeholder + return startOfDay.addingTimeInterval(.twentyFourHours / 2) + } else if isAlwaysDay { + // Midnight sun - sunset is effectively end of day + return endOfDay + } + } + return sunset + } + + var daylightDuration: TimeInterval { + if isCircumPolar { + if isAlwaysDay { + return .twentyFourHours + } else if isAlwaysNight { + return 0 + } + } + return sunset.timeIntervalSince(sunrise) + } +} + +// MARK: - Related Days +extension Sun { + var yesterday: Sun { + let yesterdayDate = calendar.date(byAdding: .day, value: -1, to: date) ?? date.addingTimeInterval(-(.twentyFourHours)) + return Sun(location: location, timeZone: timeZone, date: yesterdayDate) + } + + var tomorrow: Sun { + let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: date) ?? date.addingTimeInterval(.twentyFourHours) + return Sun(location: location, timeZone: timeZone, date: tomorrowDate) + } + + /// Creates a Sun for the previous day, reusing an existing instance if provided + func getYesterday(cachedYesterday: inout Sun?) -> Sun { + if let cached = cachedYesterday { + let yesterdayDate = calendar.date(byAdding: .day, value: -1, to: date) ?? date.addingTimeInterval(-(.twentyFourHours)) + var updated = cached + updated.setDate(yesterdayDate) + cachedYesterday = updated + return updated + } + let newYesterday = yesterday + cachedYesterday = newYesterday + return newYesterday + } + + /// Creates a Sun for the next day, reusing an existing instance if provided + func getTomorrow(cachedTomorrow: inout Sun?) -> Sun { + if let cached = cachedTomorrow { + let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: date) ?? date.addingTimeInterval(.twentyFourHours) + var updated = cached + updated.setDate(tomorrowDate) + cachedTomorrow = updated + return updated + } + let newTomorrow = tomorrow + cachedTomorrow = newTomorrow + return newTomorrow + } +} + +// MARK: - Difference Strings +extension Sun { + var compactDifferenceString: LocalizedStringKey { + compactDifferenceString(comparator: nil) + } + + /// Computes the compact difference string, optionally using a pre-computed comparator Sun + func compactDifferenceString(comparator: Sun?) -> LocalizedStringKey { + let comp = comparator ?? (date.isToday ? yesterday : Sun(location: location, timeZone: timeZone)) + let difference = daylightDuration - comp.daylightDuration + let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) + + let moreOrLess = difference >= 0 ? "+" : "-" + + return LocalizedStringKey("\(moreOrLess)\(differenceString)") + } + + var differenceString: LocalizedStringKey { + differenceString(comparator: nil) + } + + /// Computes the difference string, optionally using a pre-computed comparator Sun + func differenceString(comparator: Sun?) -> LocalizedStringKey { + let formatter = DateFormatter() + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .medium + formatter.formattingContext = .middleOfSentence + + let comp = comparator ?? (date.isToday ? yesterday : Sun(location: location, timeZone: timeZone)) + let difference = daylightDuration - comp.daylightDuration + let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) + + let moreOrLess = difference >= 0 ? NSLocalizedString("more", comment: "More daylight middle of sentence") : NSLocalizedString("less", comment: "Less daylight middle of sentence") + + var baseDateString = formatter.string(from: date) + if baseDateString.contains(/\d/) { + baseDateString = NSLocalizedString("on \(baseDateString)", comment: "Sentence fragment for nominal date") + } + + let comparatorDate = comp.date + + return LocalizedStringKey("\(differenceString) \(moreOrLess) daylight \(baseDateString) compared to \(formatter.string(from: comparatorDate))") + } +} + +// MARK: - Events +extension Sun { + var events: Array { + [ + Event(label: "Astronomical Sunrise", date: astronomicalDawn, phase: .astronomical), + Event(label: "Nautical Sunrise", date: nauticalDawn, phase: .nautical), + Event(label: "Civil Sunrise", date: civilDawn, phase: .civil), + Event(label: "Sunrise", date: safeSunrise, phase: .sunrise), + Event(label: "Solar noon", date: solarNoon, phase: .day), + Event(label: "Sunset", date: safeSunset, phase: .sunset), + Event(label: "Civil Sunset", date: civilDusk, phase: .civil), + Event(label: "Nautical Sunset", date: nauticalDusk, phase: .nautical), + Event(label: "Astronomical Sunset", date: astronomicalDusk, phase: .astronomical), + ] + .sorted { a, b in + a.date.compare(b.date) == .orderedAscending + } + } + + var nextSolarEvent: Event? { + nextSolarEvent(cachedTomorrow: nil) + } + + /// Gets the next solar event, optionally using a pre-computed tomorrow Sun + func nextSolarEvent(cachedTomorrow: Sun?) -> Event? { + events.filter { $0.phase == .sunset || $0.phase == .sunrise }.first(where: { $0.date > date }) + ?? (cachedTomorrow ?? tomorrow).events.filter { $0.phase == .sunset || $0.phase == .sunrise }.first(where: { $0.date > date }) + } + + var previousSolarEvent: Event? { + previousSolarEvent(cachedYesterday: nil) + } + + /// Gets the previous solar event, optionally using a pre-computed yesterday Sun + func previousSolarEvent(cachedYesterday: Sun?) -> Event? { + events.filter { $0.phase == .sunset || $0.phase == .sunrise }.last(where: { $0.date < date }) + ?? (cachedYesterday ?? yesterday).events.filter { $0.phase == .sunset || $0.phase == .sunrise }.last(where: { $0.date < date }) + } +} + +// MARK: - Convenience Initializer (Solar-style API) +extension Sun { + /// Creates a Sun instance using Solar-style API + /// - Parameters: + /// - date: The date for calculations + /// - coordinate: The geographic coordinate + /// - timeZone: The timezone (defaults to current) + init(for date: Date, coordinate: CLLocationCoordinate2D, timeZone: TimeZone = .current) { + let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + self.init(location: location, timeZone: timeZone, date: date) + } + + /// Creates a Sun instance for the current date at a coordinate + /// - Parameter coordinate: The geographic coordinate + init(coordinate: CLLocationCoordinate2D, timeZone: TimeZone = .current) { + self.init(for: Date(), coordinate: coordinate, timeZone: timeZone) + } +} diff --git a/Solstice/Helper Views/LandingView.swift b/Solstice/Helper Views/LandingView.swift index f0aace94..41cd419f 100644 --- a/Solstice/Helper Views/LandingView.swift +++ b/Solstice/Helper Views/LandingView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import Suite fileprivate struct SizePreferenceKey: PreferenceKey { @@ -83,19 +83,19 @@ struct LandingView: View { dynamicTypeSize > .accessibility2 } - @State private var solar = Solar(coordinate: .proxiedToTimeZone) + @State private var sun = Sun(coordinate: .proxiedToTimeZone) private let renderTime = Date.now - + var body: some View { ZStack { TimelineView(.animation) { context in - SkyGradient(solar: solar) + SkyGradient(sun: sun) .ignoresSafeArea() .task(id: context.date) { - solar = Solar( - for: renderTime.addingTimeInterval(context.date.distance(to: renderTime) * 1000), + sun = Sun( + for: renderTime.addingTimeInterval(renderTime.timeIntervalSince(context.date) * 1000), coordinate: .proxiedToTimeZone - ) ?? solar + ) } } diff --git a/Solstice/Helper Views/ShareSolarChartView.swift b/Solstice/Helper Views/ShareSolarChartView.swift index 1c41f757..4550a00e 100644 --- a/Solstice/Helper Views/ShareSolarChartView.swift +++ b/Solstice/Helper Views/ShareSolarChartView.swift @@ -6,15 +6,15 @@ // import SwiftUI -import Solar +import SunKit import Suite import TimeMachine struct ShareSolarChartView: View { @Environment(\.dismiss) var dismiss @Environment(\.timeMachine) var timeMachine: TimeMachine - - var solar: Solar + + var sun: Sun var location: Location @State var chartAppearance: DaylightChart.Appearance = .graphical @@ -30,7 +30,7 @@ struct ShareSolarChartView: View { @ViewBuilder var daylightChartView: some View { DaylightChart( - solar: solar, + sun: sun, timeZone: location.timeZone, appearance: chartAppearance, scrubbable: true, markSize: chartMarkSize @@ -38,13 +38,13 @@ struct ShareSolarChartView: View { .if(chartAppearance == .graphical) { content in content .background { - SkyGradient(solar: solar) + SkyGradient(sun: sun) } } } - + var deps: [AnyHashable] { - [showLocationName, solar.date, location, chartAppearance] + [showLocationName, sun.date, location, chartAppearance] } private let igStoriesUrl: URL? = URL(string: "instagram-stories://share?source_application=me.daneden.Solstice") @@ -153,7 +153,7 @@ struct ShareSolarChartView: View { #if os(iOS) @ViewBuilder private var instagramShareButton: some View { - let solarGradient = SkyGradient(solar: solar) + let solarGradient = SkyGradient(sun: sun) if let igStoriesUrl, let imageData, let topColor = solarGradient.stops.first?.toHex(), @@ -206,26 +206,26 @@ struct ShareSolarChartView: View { } .font(.headline) - let duration = solar.daylightDuration.localizedString + let duration = sun.daylightDuration.localizedString Text("\(duration) of daylight") .foregroundStyle(.secondary) } - + Spacer() - + VStack(alignment: .trailing) { - Label("\(solar.safeSunrise, style: .time)", systemImage: "sunrise") - Label("\(solar.safeSunset, style: .time)", systemImage: "sunset") + Label("\(sun.safeSunrise, style: .time)", systemImage: "sunrise") + Label("\(sun.safeSunset, style: .time)", systemImage: "sunset") } .foregroundStyle(.secondary) } } else { VStack(alignment: .leading) { - let duration = solar.daylightDuration.localizedString + let duration = sun.daylightDuration.localizedString Text("\(duration) of daylight") .font(.headline) - - Label("\(solar.safeSunrise...solar.safeSunset)", systemImage: "sun.max") + + Label("\(sun.safeSunrise...sun.safeSunset)", systemImage: "sun.max") .foregroundStyle(.secondary) } } @@ -281,6 +281,6 @@ struct ShareSolarChartView: View { } #Preview { - ShareSolarChartView(solar: .init(coordinate: TemporaryLocation.placeholderLondon.coordinate)!, location: TemporaryLocation.placeholderLondon) + ShareSolarChartView(sun: .init(coordinate: TemporaryLocation.placeholderLondon.coordinate), location: TemporaryLocation.placeholderLondon) .withTimeMachine(.solsticeTimeMachine) } diff --git a/Solstice/Helper Views/SkyGradient.swift b/Solstice/Helper Views/SkyGradient.swift index 9f96f673..fb3043ad 100644 --- a/Solstice/Helper Views/SkyGradient.swift +++ b/Solstice/Helper Views/SkyGradient.swift @@ -7,13 +7,11 @@ import Foundation import SwiftUI -import Solar +import SunKit import CoreLocation -extension Solar: @unchecked @retroactive Sendable {} - struct SkyGradient: View, ShapeStyle { - var solar: Solar? = Solar(coordinate: .proxiedToTimeZone) + var sun: Sun? = Sun(coordinate: .proxiedToTimeZone) static let dawn = [ Color(red: 0.388, green: 0.435, blue: 0.643), @@ -51,7 +49,7 @@ struct SkyGradient: View, ShapeStyle { } var colors: [[Color]] { - let duration = solar?.daylightDuration ?? 43200 + let duration = sun?.daylightDuration ?? 43200 let daylightHours = Int((duration / (60 * 60)) / 2) let amColors = [Self.night, Self.dawn, Self.morning] let pmColors = [Self.afternoon, Self.evening, Self.night] @@ -68,17 +66,17 @@ struct SkyGradient: View, ShapeStyle { } var stops: [Color] { - let sunrise: Date = solar?.safeSunrise ?? .now.startOfDay - let sunset: Date = solar?.safeSunset ?? .now.endOfDay - let currentDate: Date = solar?.date ?? .now + let sunrise: Date = sun?.safeSunrise ?? .now.startOfDay + let sunset: Date = sun?.safeSunset ?? .now.endOfDay + let currentDate: Date = sun?.date ?? .now let twilightDuration: TimeInterval = 60 * 180 let dayStart: Date = sunrise.addingTimeInterval(-twilightDuration) let dayEnd: Date = sunset.addingTimeInterval(twilightDuration) - let duration: TimeInterval = dayStart.distance(to: dayEnd) + let duration: TimeInterval = dayEnd.timeIntervalSince(dayStart) let progressStart: Date = sunrise.addingTimeInterval(-twilightDuration / 1.5) - let progress: Double = progressStart.distance(to: currentDate) / duration + let progress: Double = currentDate.timeIntervalSince(progressStart) / duration let colorCount: Int = colors.count let progressThroughStops: Double = progress * Double(colorCount) @@ -100,41 +98,41 @@ struct SkyGradient: View, ShapeStyle { } } -extension Solar { +extension Sun { var view: some View { - SkyGradient(solar: self) + SkyGradient(sun: self) } } fileprivate struct PreviewContainer: View { @State var date = Date.now - - var solars: [Solar] { - var result = [Solar?]() - + + var suns: [Sun] { + var result = [Sun]() + for i in stride(from: 0, to: 180, by: 15) { let location = CLLocationCoordinate2D(latitude: Double(i) - 90, longitude: 0) - result.append(Solar(for: date, coordinate: location)) + result.append(Sun(for: date, coordinate: location)) } - - return result.compactMap { $0 } + + return result } - + var body: some View { TimelineView(.animation) { t in VStack(spacing: 0) { - ForEach(solars, id: \.coordinate.latitude) { solar in + ForEach(suns, id: \.coordinate.latitude) { sun in ZStack { - SkyGradient(solar: solar) - + SkyGradient(sun: sun) + HStack { - Text(solar.date, style: .time) + Text(sun.date, style: .time) .font(.largeTitle) - + Spacer() VStack { - Text(solar.safeSunrise...solar.safeSunset) - Text(solar.daylightDuration.localizedString) + Text(sun.safeSunrise...sun.safeSunset) + Text(sun.daylightDuration.localizedString) } } .padding() diff --git a/Solstice/Helpers/NotificationManager.swift b/Solstice/Helpers/NotificationManager.swift index c67e0ee3..28150206 100644 --- a/Solstice/Helpers/NotificationManager.swift +++ b/Solstice/Helpers/NotificationManager.swift @@ -8,7 +8,7 @@ import Foundation import UserNotifications import CoreLocation -import Solar +import SunKit import SwiftUI import CoreData #if os(iOS) && !WIDGET_EXTENSION @@ -86,11 +86,9 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { for i in 0...63 { let date = calendar.date(byAdding: .day, value: i, to: Date()) ?? .now - guard let solar = Solar(for: date, coordinate: location.coordinate) else { - continue - } + let sun = Sun(for: date, coordinate: location.coordinate, timeZone: timeZone) - let notificationDate = getNextNotificationDate(after: date, with: solar) + let notificationDate = getNextNotificationDate(after: date, with: sun) guard let notificationContent = buildNotificationContent(for: notificationDate, location: location, timeZone: timeZone) else { return @@ -150,14 +148,14 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { } #endif - static func getNextNotificationDate(after date: Date, with solar: Solar? = nil) -> Date { + static func getNextNotificationDate(after date: Date, with sun: Sun? = nil) -> Date { if scheduleType == .specificTime { let hour = notificationDateComponents.hour ?? 0 let minute = notificationDateComponents.minute ?? 0 return calendar.date(bySettingHour: hour, minute: minute, second: 0, of: date) ?? date } else { - guard let solar else { return date } - let relativeDate = scheduleType == .sunset ? solar.safeSunset : solar.safeSunrise + guard let sun else { return date } + let relativeDate = scheduleType == .sunset ? sun.safeSunset : sun.safeSunrise let offsetDate = relativeDate.addingTimeInterval(userPreferenceNotificationOffset) let scheduleComponents = calendar.dateComponents([.hour, .minute], from: offsetDate) return calendar.date( @@ -180,9 +178,9 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { /// don't alter notification previews. /// - Returns: A `NotificationContent` object appropriate for the context static func buildNotificationContent(for date: Date, location: CLLocation, timeZone: TimeZone = .autoupdatingCurrent, in context: Context = .notification) -> NotificationContent? { - guard let solar = Solar(for: date, coordinate: location.coordinate) else { return nil } + let sun = Sun(for: date, coordinate: location.coordinate, timeZone: timeZone) - let difference = solar.daylightDuration - (solar.yesterday?.daylightDuration ?? 0) + let difference = sun.daylightDuration - sun.yesterday.daylightDuration // Check if we should suppress the notification entirely (SAD preference) if shouldSuppressNotification(difference: difference, context: context) { @@ -190,7 +188,7 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { } let title = buildNotificationTitle(for: date) - let body = buildNotificationBody(solar: solar, timeZone: timeZone, difference: difference, date: date, context: context) + let body = buildNotificationBody(sun: sun, timeZone: timeZone, difference: difference, date: date, context: context) return NotificationContent(title: title, body: body) } @@ -259,13 +257,13 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { // MARK: Body Generation /// Builds the notification body with all enabled content fragments - private static func buildNotificationBody(solar: Solar, timeZone: TimeZone, difference: TimeInterval, date: Date, context: Context) -> String { - let duration = solar.daylightDuration.localizedString + private static func buildNotificationBody(sun: Sun, timeZone: TimeZone, difference: TimeInterval, date: Date, context: Context) -> String { + let duration = sun.daylightDuration.localizedString let differenceString = difference.localizedString @StringBuilder var body: String { if includeSunTimes { - sunTimesFragment(solar: solar, timeZone: timeZone) + sunTimesFragment(sun: sun, timeZone: timeZone) } if includeDaylightDuration { @@ -293,9 +291,9 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { // MARK: Body Fragments - private static func sunTimesFragment(solar: Solar, timeZone: TimeZone) -> String { - let sunriseTime = solar.safeSunrise.withTimeZoneAdjustment(for: timeZone).formatted(.dateTime.hour().minute()) - let sunsetTime = solar.safeSunset.withTimeZoneAdjustment(for: timeZone).formatted(.dateTime.hour().minute()) + private static func sunTimesFragment(sun: Sun, timeZone: TimeZone) -> String { + let sunriseTime = sun.safeSunrise.withTimeZoneAdjustment(for: timeZone).formatted(.dateTime.hour().minute()) + let sunsetTime = sun.safeSunset.withTimeZoneAdjustment(for: timeZone).formatted(.dateTime.hour().minute()) let format = NSLocalizedString( "notif-sunrise-sunset", value: "The sun rises at %1$@ and sets at %2$@.", diff --git a/Solstice/List View/GraphicalLocationListRow.swift b/Solstice/List View/GraphicalLocationListRow.swift index 75879eef..40db1802 100644 --- a/Solstice/List View/GraphicalLocationListRow.swift +++ b/Solstice/List View/GraphicalLocationListRow.swift @@ -7,16 +7,16 @@ import SwiftUI import TimeMachine -import Solar +import SunKit struct GraphicalLocationListRow: View { @Environment(\.timeMachine) private var timeMachine var location: Location - - var solar: Solar? { - Solar(for: timeMachine.date, coordinate: location.coordinate) + + var sun: Sun? { + Sun(for: timeMachine.date, coordinate: location.coordinate) } - + var body: some View { LocationListRow(location: location, headingFontWeight: .semibold) .foregroundStyle(.white) @@ -25,7 +25,7 @@ struct GraphicalLocationListRow: View { .shadow(color: .black.opacity(0.3), radius: 6, y: 2) .padding() .background { - solar?.view + sun?.view .clipShape(.rect(cornerRadius: 20, style: .continuous)) } .listRowSeparator(.hidden) diff --git a/Solstice/List View/LocationListRow.swift b/Solstice/List View/LocationListRow.swift index b74c0a9b..2e67106b 100644 --- a/Solstice/List View/LocationListRow.swift +++ b/Solstice/List View/LocationListRow.swift @@ -6,22 +6,22 @@ // import SwiftUI -import Solar +import SunKit import Suite import TimeMachine struct LocationListRow: View { @Environment(\.timeMachine) private var timeMachine: TimeMachine var location: Location - + @FocusState private var focused: Bool - + @State private var showRemainingDaylight = false - + var headingFontWeight: Font.Weight = .medium - - private var solar: Solar? { - Solar(for: timeMachine.date, coordinate: location.coordinate) + + private var sun: Sun? { + Sun(for: timeMachine.date, coordinate: location.coordinate) } private var isCurrentLocation: Bool { @@ -38,16 +38,16 @@ struct LocationListRow: View { @ViewBuilder var trailingContent: some View { - if let solar { + if let sun { VStack(alignment: .trailing) { - Text(Duration.seconds(solar.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) + Text(Duration.seconds(sun.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) #if os(iOS) .font(.headline.weight(headingFontWeight)) #endif - - let sunrise = solar.safeSunrise.withTimeZoneAdjustment(for: location.timeZone) - let sunset = solar.safeSunset.withTimeZoneAdjustment(for: location.timeZone) - + + let sunrise = sun.safeSunrise.withTimeZoneAdjustment(for: location.timeZone) + let sunset = sun.safeSunset.withTimeZoneAdjustment(for: location.timeZone) + if sunrise < sunset { Text(sunrise...sunset) .foregroundStyle(.secondary) @@ -72,12 +72,12 @@ struct LocationListRow: View { } } - if let solar { + if let sun { Group { - Text(Duration.seconds(solar.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) + Text(Duration.seconds(sun.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) .font(.headline) .contentTransition(.numericText()) - Text(solar.safeSunrise.withTimeZoneAdjustment(for: location.timeZone)...solar.safeSunset.withTimeZoneAdjustment(for: location.timeZone)) + Text(sun.safeSunrise.withTimeZoneAdjustment(for: location.timeZone)...sun.safeSunset.withTimeZoneAdjustment(for: location.timeZone)) .font(.footnote) .foregroundStyle(.secondary) .contentTransition(.numericText()) diff --git a/Solstice/List View/SidebarListView.swift b/Solstice/List View/SidebarListView.swift index 8816c367..3bd5636f 100644 --- a/Solstice/List View/SidebarListView.swift +++ b/Solstice/List View/SidebarListView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import TimeMachine struct SidebarListView: View { @@ -116,9 +116,8 @@ struct SidebarListView: View { private func savedLocationPreview(for item: SavedLocation) -> some View { Form { - if let solar = Solar(for: timeMachine.date, coordinate: item.coordinate) { - DailyOverview(solar: solar, location: item) - } + let sun = Sun(for: timeMachine.date, coordinate: item.coordinate) + DailyOverview(sun: sun, location: item) } .withTimeMachine(.solsticeTimeMachine) } @@ -167,16 +166,14 @@ extension SidebarListView { return lhs.timeZone.secondsFromGMT() > rhs.timeZone.secondsFromGMT() } case .daylightDuration: - guard let lhsSolar = Solar(for: timeMachine.date, coordinate: lhs.coordinate), - let rhsSolar = Solar(for: timeMachine.date, coordinate: rhs.coordinate) else { - return true - } - + let lhsSun = Sun(for: timeMachine.date, coordinate: lhs.coordinate) + let rhsSun = Sun(for: timeMachine.date, coordinate: rhs.coordinate) + switch itemSortOrder { case .forward: - return lhsSolar.daylightDuration < rhsSolar.daylightDuration + return lhsSun.daylightDuration < rhsSun.daylightDuration case .reverse: - return lhsSolar.daylightDuration > rhsSolar.daylightDuration + return lhsSun.daylightDuration > rhsSun.daylightDuration } } } diff --git a/Solstice/Settings/AboutSolsticeView.swift b/Solstice/Settings/AboutSolsticeView.swift index e44aaba6..38780035 100644 --- a/Solstice/Settings/AboutSolsticeView.swift +++ b/Solstice/Settings/AboutSolsticeView.swift @@ -88,10 +88,10 @@ fileprivate struct FullStoryView: View { } } - if let url = URL(string: "https://github.com/ceeK/Solar") { + if let url = URL(string: "https://github.com/Sunlight-dev/SunKit") { Section { Link(destination: url) { - Text("ceeK/Solar") + Text("Sunlight-dev/SunKit") } } header: { Text("Open Source Acknowledgements") diff --git a/Solstice/Settings/NotificationSettings.swift b/Solstice/Settings/NotificationSettings.swift index d0a7778b..d6e6f163 100644 --- a/Solstice/Settings/NotificationSettings.swift +++ b/Solstice/Settings/NotificationSettings.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import CoreLocation fileprivate typealias NotificationFragment = (label: String, value: Binding) diff --git a/Widget/Countdown Widget/CountdownWidget.swift b/Widget/Countdown Widget/CountdownWidget.swift index 9b16d2f1..5a252b68 100644 --- a/Widget/Countdown Widget/CountdownWidget.swift +++ b/Widget/Countdown Widget/CountdownWidget.swift @@ -7,7 +7,7 @@ import WidgetKit import SwiftUI -import Solar +import SunKit struct CountdownWidget: Widget { #if os(iOS) @@ -17,7 +17,7 @@ struct CountdownWidget: Widget { #elseif os(watchOS) static var supportedFamilies: [WidgetFamily] = [.accessoryInline, .accessoryCircular, .accessoryRectangular, .accessoryCorner] #endif - + var body: some WidgetConfiguration { IntentConfiguration( kind: SolsticeWidgetKind.CountdownWidget.rawValue, @@ -26,7 +26,8 @@ struct CountdownWidget: Widget { ) { timelineEntry in CountdownWidgetView(entry: timelineEntry) .containerBackground(for: .widget) { - SkyGradient(solar: Solar(for: timelineEntry.date, coordinate: (timelineEntry.location ?? .defaultLocation).coordinate)!) + let location = timelineEntry.location ?? .defaultLocation + SkyGradient(sun: Sun(for: timelineEntry.date, coordinate: location.coordinate, timeZone: location.timeZone)) } .widgetURL(timelineEntry.location?.url) } diff --git a/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift b/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift index 19da6d7f..f1124dd6 100644 --- a/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift +++ b/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift @@ -7,13 +7,13 @@ #if !os(macOS) import SwiftUI -import Solar +import SunKit import WidgetKit import Suite extension CountdownWidgetView { struct AccessoryInlineView: View { - var nextEvent: Solar.Event + var nextEvent: Sun.Event var body: some View { HStack { Image(systemName: nextEvent.imageName) @@ -27,14 +27,17 @@ extension CountdownWidgetView { var entryDate: Date - var previousEvent: Solar.Event - var nextEvent: Solar.Event + var previousEvent: Sun.Event + var nextEvent: Sun.Event + + var durationToNextEvent: TimeInterval { + nextEvent.date.timeIntervalSince(entryDate) + } @ViewBuilder var currentValueLabel: some View { - let duration = entryDate.distance(to: nextEvent.date) - if duration >= 60 * 60 { - Text(Duration.seconds(duration).formatted(.units(width: .narrow, maximumUnitCount: 1))) + if durationToNextEvent >= 60 * 60 { + Text(Duration.seconds(durationToNextEvent).formatted(.units(width: .narrow, maximumUnitCount: 1))) } else { Text(nextEvent.date, style: .timer) .monospacedDigit() @@ -65,7 +68,7 @@ extension CountdownWidgetView { } struct AccessoryRectangularView: View { - var nextEvent: Solar.Event + var nextEvent: Sun.Event var body: some View { HStack { @@ -89,8 +92,8 @@ extension CountdownWidgetView { } struct AccessoryCornerView: View { - var previousEvent: Solar.Event - var nextEvent: Solar.Event + var previousEvent: Sun.Event + var nextEvent: Sun.Event var body: some View { Label { diff --git a/Widget/Countdown Widget/CountdownWidgetView.swift b/Widget/Countdown Widget/CountdownWidgetView.swift index 20717098..6ef617ed 100644 --- a/Widget/Countdown Widget/CountdownWidgetView.swift +++ b/Widget/Countdown Widget/CountdownWidgetView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import WidgetKit import Suite @@ -16,7 +16,7 @@ struct CountdownWidgetView: SolsticeWidgetView { @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground var entry: SolsticeWidgetTimelineEntry - + var body: some View { if let location, let nextSolarEvent, @@ -82,18 +82,18 @@ struct CountdownWidgetView: SolsticeWidgetView { } extension CountdownWidgetView { - var nextSolarEvent: Solar.Event? { - solar?.nextSolarEvent + var nextSolarEvent: Sun.Event? { + sun?.nextSolarEvent } - - var previousSolarEvent: Solar.Event? { - solar?.previousSolarEvent + + var previousSolarEvent: Sun.Event? { + sun?.previousSolarEvent } - + var timeZone: TimeZone { location?.timeZone ?? .autoupdatingCurrent } - + var nextEventText: some View { if let nextSolarEvent { return Text("\(nextSolarEvent.description.localizedCapitalized) in \(Text(nextSolarEvent.date, style: .relative))") @@ -101,7 +101,7 @@ extension CountdownWidgetView { return Text("—") } } - + var currentEventImageName: String { nextSolarEvent?.phase == .sunrise ? "moon.stars" : "sun.max" } @@ -113,7 +113,7 @@ struct CountdownWidgetPreview: PreviewProvider { CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) .previewContext(WidgetPreviewContext(family: .systemSmall)) #endif - + #if os(watchOS) || os(iOS) CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) diff --git a/Widget/Helpers/SolsticeWidgetTimelineProvider.swift b/Widget/Helpers/SolsticeWidgetTimelineProvider.swift index 9630a8fc..4197b743 100644 --- a/Widget/Helpers/SolsticeWidgetTimelineProvider.swift +++ b/Widget/Helpers/SolsticeWidgetTimelineProvider.swift @@ -7,13 +7,18 @@ import WidgetKit import CoreLocation -import Solar +import SunKit struct SolsticeWidgetTimelineEntry: TimelineEntry { let date: Date var location: SolsticeWidgetLocation? var relevance: TimelineEntryRelevance? var locationError: LocationError? + + /// Pre-computed Sun for this entry's date and location (avoids recomputation in views) + var cachedSun: Sun? + /// Pre-computed tomorrow Sun for this entry (avoids recomputation in views) + var cachedTomorrowSun: Sun? } enum LocationError: Error { @@ -58,7 +63,7 @@ struct SolsticeTimelineProvider: IntentTimelineProvider { guard SolsticeWidgetLocationManager.isAuthorized else { return (nil, isRealLocation, .notAuthorized) } - + location = await SolsticeWidgetLocationManager.shared.getLocation() } @@ -69,7 +74,7 @@ struct SolsticeTimelineProvider: IntentTimelineProvider { guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else { return (nil, isRealLocation, .reverseGeocodingFailed) } - + return (placemark, isRealLocation, nil) } @@ -78,10 +83,20 @@ struct SolsticeTimelineProvider: IntentTimelineProvider { let (placemark, isRealLocation, error) = await fetchWidgetLocation(for: configuration) let widgetLocation = getLocation(for: placemark, isRealLocation: isRealLocation) let resolvedLocation = widgetLocation ?? (context.isPreview ? .proxiedToTimeZone : nil) + // Pre-compute Sun for snapshot + var cachedSun: Sun? + var cachedTomorrowSun: Sun? + if let coord = resolvedLocation?.coordinate { + let tz = resolvedLocation?.timeZone ?? .current + cachedSun = Sun(for: Date(), coordinate: coord, timeZone: tz) + cachedTomorrowSun = cachedSun?.tomorrow + } let entry = SolsticeWidgetTimelineEntry( date: Date(), location: resolvedLocation, - locationError: context.isPreview ? nil : error + locationError: context.isPreview ? nil : error, + cachedSun: cachedSun, + cachedTomorrowSun: cachedTomorrowSun ) completion(entry) } @@ -95,48 +110,48 @@ struct SolsticeTimelineProvider: IntentTimelineProvider { let currentDate = Date() let calendar = Calendar.current - guard let coordinate = widgetLocation?.coordinate, - let todaySolar = Solar(for: currentDate, coordinate: coordinate) else { + guard let coordinate = widgetLocation?.coordinate else { return completion( Timeline( entries: [ - SolsticeWidgetTimelineEntry(date: currentDate, location: widgetLocation, locationError: error) + SolsticeWidgetTimelineEntry(date: currentDate, location: widgetLocation, locationError: error, cachedSun: nil, cachedTomorrowSun: nil) ], policy: .after(.now.addingTimeInterval(60 * 15)) ) ) } + let timeZone = widgetLocation?.timeZone ?? .current + let todaySun = Sun(for: currentDate, coordinate: coordinate, timeZone: timeZone) + // Generate entries for today + next 2 days (3 days total) let daysToGenerate = 3 var allKeyTimes: [Date] = [currentDate] var allHourlyTimes: [Date] = [] - var solarByDay: [Date: Solar] = [:] + var sunByDay: [Date: Sun] = [:] let today = calendar.startOfDay(for: currentDate) for dayOffset in 0.. Entry { let dayStart = calendar.startOfDay(for: date) - let solar = solarByDay[dayStart] ?? todaySolar + var sun = sunByDay[dayStart] ?? todaySun + + // Update the sun's date to the entry time (reuses cached calculations if same day) + sun.setDate(date) - let distanceToSunrise = abs(date.distance(to: solar.safeSunrise)) - let distanceToSunset = abs(date.distance(to: solar.safeSunset)) + let distanceToSunrise = abs(sun.safeSunrise.timeIntervalSince(date)) + let distanceToSunset = abs(sun.safeSunset.timeIntervalSince(date)) let nearestEventDistance = min(distanceToSunset, distanceToSunrise) let relevance: TimelineEntryRelevance? = nearestEventDistance < (60 * 30) ? .init(score: 10, duration: nearestEventDistance) : nil + // Pre-compute tomorrow sun for widgets that need it + let tomorrowStart = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart + let tomorrowSun = sunByDay[tomorrowStart] ?? sun.tomorrow + return Entry( date: date, location: widgetLocation, - relevance: relevance + relevance: relevance, + cachedSun: sun, + cachedTomorrowSun: tomorrowSun ) } @@ -212,32 +236,42 @@ struct SolsticeTimelineProvider: IntentTimelineProvider { return completion(Timeline(entries: entries, policy: .after(lastEntryDate))) } } - + func placeholder(in context: Context) -> SolsticeWidgetTimelineEntry { - SolsticeWidgetTimelineEntry(date: Date(), location: .defaultLocation) + let sun = Sun(for: Date(), coordinate: SolsticeWidgetLocation.defaultLocation.coordinate, timeZone: SolsticeWidgetLocation.defaultLocation.timeZone) + return SolsticeWidgetTimelineEntry(date: Date(), location: .defaultLocation, cachedSun: sun, cachedTomorrowSun: sun.tomorrow) } } extension SolsticeWidgetTimelineEntry { + /// Helper to create an entry with pre-computed Sun + static func withSun(date: Date, location: SolsticeWidgetLocation?, locationError: LocationError? = nil) -> SolsticeWidgetTimelineEntry { + guard let loc = location else { + return SolsticeWidgetTimelineEntry(date: date, location: nil, locationError: locationError, cachedSun: nil, cachedTomorrowSun: nil) + } + let sun = Sun(for: date, coordinate: loc.coordinate, timeZone: loc.timeZone) + return SolsticeWidgetTimelineEntry(date: date, location: loc, locationError: locationError, cachedSun: sun, cachedTomorrowSun: sun.tomorrow) + } + static func previewTimeline() async -> [SolsticeWidgetTimelineEntry] { [ - SolsticeWidgetTimelineEntry(date: .now, location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 6), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 12), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 18), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 24), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 30), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(1), location: nil), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(2), location: nil, locationError: .locationUpdateFailed), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(3), location: nil, locationError: .notAuthorized), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(4), location: nil, locationError: .reverseGeocodingFailed), + .withSun(date: .now, location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 6), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 12), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 18), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 24), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 30), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 36), location: .defaultLocation), + .withSun(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(1), location: nil), + .withSun(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(2), location: nil, locationError: .locationUpdateFailed), + .withSun(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(3), location: nil, locationError: .notAuthorized), + .withSun(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(4), location: nil, locationError: .reverseGeocodingFailed), ] } - + static var placeholder: Self { - SolsticeWidgetTimelineEntry(date: .now, location: .proxiedToTimeZone) + .withSun(date: .now, location: .proxiedToTimeZone) } } diff --git a/Widget/Helpers/SolsticeWidgetView.swift b/Widget/Helpers/SolsticeWidgetView.swift index e12e70ae..b538b36b 100644 --- a/Widget/Helpers/SolsticeWidgetView.swift +++ b/Widget/Helpers/SolsticeWidgetView.swift @@ -5,29 +5,35 @@ // Created by Daniel Eden on 20/01/2026. // import SwiftUI -import Solar +import SunKit protocol SolsticeWidgetView: View { var entry: SolsticeWidgetTimelineEntry { get set } } extension SolsticeWidgetView { - var solar: Solar? { + /// Uses pre-computed Sun from timeline entry if available, otherwise computes (fallback) + var sun: Sun? { + if let cached = entry.cachedSun { + return cached + } + // Fallback for backwards compatibility guard let location else { return nil } - return Solar(for: entry.date, coordinate: location.coordinate) + return Sun(for: entry.date, coordinate: location.coordinate, timeZone: location.timeZone) } - var tomorrowSolar: Solar? { - solar?.tomorrow + /// Uses pre-computed tomorrow Sun from timeline entry if available + var tomorrowSun: Sun? { + entry.cachedTomorrowSun ?? sun?.tomorrow } - var relevantSolar: Solar? { - isAfterTodaySunset ? tomorrowSolar : solar + var relevantSun: Sun? { + isAfterTodaySunset ? tomorrowSun : sun } var isAfterTodaySunset: Bool { - guard let solar else { return false } - return solar.safeSunset < entry.date + guard let sun else { return false } + return sun.safeSunset < entry.date } var location: SolsticeWidgetLocation? { @@ -38,7 +44,7 @@ extension SolsticeWidgetView { /// True when we have valid data to display var hasValidData: Bool { - location != nil && solar != nil + location != nil && sun != nil } /// True when we should show a placeholder/redacted view (temporary location failure) diff --git a/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift b/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift index 22e302f9..cbe67e89 100644 --- a/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift +++ b/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift @@ -8,18 +8,18 @@ #if !os(macOS) import SwiftUI import WidgetKit -import Solar +import SunKit import Suite extension OverviewWidgetView { struct AccessoryCircularView: View { @Environment(\.widgetRenderingMode) var renderingMode - var solar: Solar + var sun: Sun var location: SolsticeWidgetLocation var body: some View { DaylightChart( - solar: solar, + sun: sun, timeZone: location.timeZone, showEventTypes: false, appearance: renderingMode == .fullColor ? .graphical : .simple, @@ -31,7 +31,7 @@ extension OverviewWidgetView { .background { AccessoryWidgetBackground() } .mask(Circle()) .widgetLabel { - Label(solar.daylightDuration.localizedString, systemImage: "sun.max") + Label(sun.daylightDuration.localizedString, systemImage: "sun.max") } } } @@ -39,8 +39,8 @@ extension OverviewWidgetView { struct AccessoryRectangularView: View { var isAfterTodaySunset: Bool var location: SolsticeWidgetLocation? - var relevantSolar: Solar? - var comparisonSolar: Solar? + var relevantSun: Sun? + var comparisonSun: Sun? var body: some View { HStack { @@ -52,16 +52,16 @@ extension OverviewWidgetView { .allowsTightening(true) .contentTransition(.interpolate) - if let relevantSolar { - Text(relevantSolar.daylightDuration.localizedString) + if let relevantSun { + Text(relevantSun.daylightDuration.localizedString) .contentTransition(.numericText()) Group { - if let comparisonSolar { - let difference = relevantSolar.daylightDuration - comparisonSolar.daylightDuration + if let comparisonSun { + let difference = relevantSun.daylightDuration - comparisonSun.daylightDuration Text("\(difference >= 0 ? "+" : "-")\(Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)))") } else { - Text(relevantSolar.safeSunrise...relevantSolar.safeSunset) + Text(relevantSun.safeSunrise...relevantSun.safeSunset) } } .foregroundStyle(.secondary) diff --git a/Widget/Overview Widget/OverviewWidgetView.swift b/Widget/Overview Widget/OverviewWidgetView.swift index b668c650..7b197017 100644 --- a/Widget/Overview Widget/OverviewWidgetView.swift +++ b/Widget/Overview Widget/OverviewWidgetView.swift @@ -7,7 +7,7 @@ import SwiftUI import WidgetKit -import Solar +import SunKit struct OverviewWidgetView: SolsticeWidgetView { @Environment(\.widgetRenderingMode) private var renderingMode @@ -18,20 +18,20 @@ struct OverviewWidgetView: SolsticeWidgetView { var body: some View { Group { - if let solar, + if let sun, let location { switch family { #if os(watchOS) || os(iOS) case .accessoryCircular: - AccessoryCircularView(solar: solar, location: location) + AccessoryCircularView(sun: sun, location: location) case .accessoryInline: - Label(solar.daylightDuration.localizedString, systemImage: "sun.max") + Label(sun.daylightDuration.localizedString, systemImage: "sun.max") case .accessoryRectangular: AccessoryRectangularView( isAfterTodaySunset: isAfterTodaySunset, location: location, - relevantSolar: relevantSolar, - comparisonSolar: isAfterTodaySunset ? solar : nil + relevantSun: relevantSun, + comparisonSun: isAfterTodaySunset ? sun : nil ) #if os(watchOS) case .accessoryCorner: @@ -40,7 +40,7 @@ struct OverviewWidgetView: SolsticeWidgetView { .symbolVariant(.fill) .imageScale(.large) .widgetLabel { - Text(solar.daylightDuration.localizedString) + Text(sun.daylightDuration.localizedString) .widgetAccentable() } #endif // end watchOS @@ -73,11 +73,11 @@ extension OverviewWidgetView { ZStack(alignment: .bottomLeading) { #if !os(watchOS) if family != .systemSmall, - let solar, + let sun, let location { GeometryReader { geom in DaylightChart( - solar: solar, + sun: sun, timeZone: location.timeZone, showEventTypes: false, includesSummaryTitle: false, @@ -109,7 +109,7 @@ extension OverviewWidgetView { Spacer() - if let duration = relevantSolar?.daylightDuration.localizedString { + if let duration = relevantSun?.daylightDuration.localizedString { if sizeCategory < .xLarge { Group { if isAfterTodaySunset { @@ -130,9 +130,9 @@ extension OverviewWidgetView { Group { if let location, - let begins = relevantSolar?.safeSunrise.withTimeZoneAdjustment(for: location.timeZone), - let ends = relevantSolar?.safeSunset.withTimeZoneAdjustment(for: location.timeZone) { - if let differenceString = relevantSolar?.compactDifferenceString { + let begins = relevantSun?.safeSunrise.withTimeZoneAdjustment(for: location.timeZone), + let ends = relevantSun?.safeSunset.withTimeZoneAdjustment(for: location.timeZone) { + if let differenceString = relevantSun?.compactDifferenceString { Text(differenceString) .lineLimit(4) .font(.footnote) diff --git a/Widget/Solar Chart Widget/SolarChartWidgetView.swift b/Widget/Solar Chart Widget/SolarChartWidgetView.swift index 21fe19ea..39fab381 100644 --- a/Widget/Solar Chart Widget/SolarChartWidgetView.swift +++ b/Widget/Solar Chart Widget/SolarChartWidgetView.swift @@ -7,19 +7,19 @@ import SwiftUI import WidgetKit -import Solar +import SunKit struct SolarChartWidgetView: SolsticeWidgetView { var entry: SolsticeWidgetTimelineEntry var body: some View { Group { - if let solar, + if let sun, let location { ZStack(alignment: .topLeading) { HStack { Label { - Text(solar.safeSunrise.withTimeZoneAdjustment(for: location.timeZone), style: .time) + Text(sun.safeSunrise.withTimeZoneAdjustment(for: location.timeZone), style: .time) } icon: { Image(systemName: "sunrise") } @@ -28,7 +28,7 @@ struct SolarChartWidgetView: SolsticeWidgetView { Spacer() Label { - Text(solar.safeSunset.withTimeZoneAdjustment(for: location.timeZone), style: .time) + Text(sun.safeSunset.withTimeZoneAdjustment(for: location.timeZone), style: .time) } icon: { Image(systemName: "sunset") } @@ -41,7 +41,7 @@ struct SolarChartWidgetView: SolsticeWidgetView { .contentTransition(.numericText()) DaylightChart( - solar: solar, + sun: sun, timeZone: location.timeZone, showEventTypes: false, includesSummaryTitle: false, diff --git a/Widget/Solar Chart Widget/SundialWidgetView.swift b/Widget/Solar Chart Widget/SundialWidgetView.swift index 6d023f0a..4d24d46d 100644 --- a/Widget/Solar Chart Widget/SundialWidgetView.swift +++ b/Widget/Solar Chart Widget/SundialWidgetView.swift @@ -7,7 +7,7 @@ import SwiftUI import WidgetKit -import Solar +import SunKit struct SundialWidgetView: SolsticeWidgetView { var entry: SolsticeWidgetTimelineEntry @@ -42,7 +42,7 @@ struct SundialWidgetView: SolsticeWidgetView { CircularSolarChart(date: entry.date, location: location) } .containerBackground(for: .widget) { - solar?.view.opacity(0.15) + sun?.view.opacity(0.15) } } else if shouldShowPlaceholder { SundialWidgetView(entry: .placeholder) diff --git a/watchOS/ContentView.swift b/watchOS/ContentView.swift index 94dfc9fc..b0a66be4 100644 --- a/watchOS/ContentView.swift +++ b/watchOS/ContentView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import Solar +import SunKit import TimeMachine struct ContentView: View { @@ -48,18 +48,18 @@ struct ContentView: View { LocationListRow(location: currentLocation) .tag(currentLocation.id) .listRowBackground( - Solar(for: timeMachine.date, coordinate: currentLocation.coordinate)? + Sun(for: timeMachine.date, coordinate: currentLocation.coordinate) .view .clipShape(.rect(cornerRadius: 20, style: .continuous)) ) } - + ForEach(sortedItems) { item in if let tag = item.uuid?.uuidString { LocationListRow(location: item) .tag(tag) .listRowBackground( - Solar(for: timeMachine.date, coordinate: item.coordinate)? + Sun(for: timeMachine.date, coordinate: item.coordinate) .view .clipShape(.rect(cornerRadius: 20, style: .continuous)) ) @@ -76,18 +76,14 @@ struct ContentView: View { case currentLocation.id: DetailView(location: currentLocation) .containerBackground(for: .navigation) { - if let solar = Solar(for: timeMachine.date, coordinate: currentLocation.coordinate) { - solar.view - } + Sun(for: timeMachine.date, coordinate: currentLocation.coordinate).view } .timeTravelToolbar() case .some(let id): if let item = items.first(where: { $0.uuid?.uuidString == id }) { DetailView(location: item) .containerBackground(for: .navigation) { - if let solar = Solar(for: timeMachine.date, coordinate: item.coordinate) { - solar.view - } + Sun(for: timeMachine.date, coordinate: item.coordinate).view } .timeTravelToolbar() } else {