Created
September 5, 2025 19:14
-
-
Save MaherSafadii/4b28ba557f9e51051ac29645ca17b952 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// Aurora Weather | |
// | |
// Created by maher safadi on 18/10/2023. | |
// | |
import AlertKit | |
import ColorfulX | |
import CoreLocation | |
import Drops | |
import IrregularGradient | |
import SwiftData | |
import SwiftUI | |
import SwiftUIPager | |
import WeatherKit | |
struct WeatherView: View { | |
@State var viewModel: WeatherViewModel = .init() | |
@Environment(\.modelContext) private var context | |
@Query var weatherList: [WeatherModel] | |
var selectedWeather: WeatherModel { | |
viewModel.selectedWeatherModel ?? viewModel.currentLocationWeather | |
} | |
private var hasCachedData: Bool { | |
if selectedWeather.currentWeather != nil | |
|| selectedWeather.hourlyWeather != nil | |
|| selectedWeather.dailyWeather != nil | |
{ | |
return true | |
} | |
if selectedWeather.persistedCurrent != nil | |
|| !selectedWeather.persistedHourly.isEmpty | |
|| !selectedWeather.persistedDaily.isEmpty | |
{ | |
return true | |
} | |
return false | |
} | |
// Helpers to access current weather data with snapshot fallback | |
private var currentSymbolName: String? { | |
selectedWeather.currentWeather?.symbolName | |
?? selectedWeather.persistedCurrent?.symbolName | |
} | |
private var currentTempString: String { | |
if let measurement = selectedWeather.currentWeather?.temperature { | |
return String( | |
measurement.description.split(separator: ".").prefix(1).joined() | |
) | |
} else if let c = selectedWeather.persistedCurrent?.temperatureC { | |
return String(Int(c.rounded())) | |
} else { | |
return "0" | |
} | |
} | |
private var apparentTempString: String { | |
if let measurement = selectedWeather.currentWeather?.apparentTemperature | |
{ | |
return String( | |
measurement.description.split(separator: ".").prefix(1).joined() | |
) | |
} else if let c = selectedWeather.persistedCurrent?.apparentTemperatureC | |
{ | |
return String(Int(c.rounded())) | |
} else { | |
return "0" | |
} | |
} | |
private var currentConditionText: String { | |
selectedWeather.currentWeather?.condition.description ?? selectedWeather | |
.persistedCurrent?.condition ?? "-" | |
} | |
private var isDaylight: Bool { | |
selectedWeather.currentWeather?.isDaylight ?? selectedWeather | |
.persistedCurrent?.isDaylight ?? true | |
} | |
var body: some View { | |
NavigationStack { | |
ZStack { | |
IrregularGradient( | |
colors: selectedWeather.weatherColors.colorArray, | |
background: selectedWeather.weatherBackgroundColor.color, | |
speed: 0.3, | |
animate: viewModel.startAnimatingBackground | |
) | |
.id(selectedWeather.weatherColors) | |
.onAppear { | |
// Animate background as before | |
viewModel.startAnimatingBackground = false | |
DispatchQueue.main.async { | |
viewModel.startAnimatingBackground = true | |
} | |
// If we have cached data, don't show skeleton | |
if hasCachedData { | |
viewModel.isLoading = false | |
} | |
} | |
// ColorfulView( | |
// color: selectedWeather.weatherColors.colorArray + [selectedWeather.weatherBackgroundColor.color,selectedWeather.weatherBackgroundColor.color,selectedWeather.weatherBackgroundColor.color,selectedWeather.weatherBackgroundColor.color,], | |
// speed: .constant(0.15), | |
// bias: .constant(0.001), | |
// noise: .constant(0.5), | |
// repeats: false | |
// | |
// ) | |
.ignoresSafeArea(.all) | |
if viewModel.isLoading { | |
ProgressView() | |
.task { | |
do { | |
try await viewModel.getWeather( | |
for: selectedWeather | |
) | |
} catch { | |
Drops.show( | |
AlertManager.shared.failedToGetWeatherError | |
) | |
} | |
} | |
} | |
ScrollView{ | |
VStack(spacing: 0) { | |
Text(selectedWeather.cityName) | |
.minimumScaleFactor(0.1) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.font(.largeTitle) | |
.padding([.top, .horizontal]) | |
Spacer() | |
HStack(alignment: .bottom) { | |
if let weatherIcon = currentSymbolName { | |
Text(Image(systemName: weatherIcon)) | |
.font(.system(size: 62)) | |
.minimumScaleFactor(0.1) | |
.offset(CGSize(width: 0, height: -5)) | |
} | |
Text("\(currentTempString)°") | |
.font(.system(size: 70)) | |
.minimumScaleFactor(0.1) | |
VStack { | |
Text("Feels \(apparentTempString)°") | |
.frame( | |
maxWidth: .infinity, | |
alignment: .trailing | |
) | |
.font(.title3) | |
Text(currentConditionText) | |
.frame( | |
maxWidth: .infinity, | |
alignment: .trailing | |
) | |
.fontWeight(.medium) | |
.font(.title3) | |
} | |
.padding(.bottom, 17) | |
} | |
.padding(.horizontal) | |
ScrollView(.vertical, showsIndicators: false) { | |
LazyVStack(spacing: 0) { | |
// Hourly Weather Strip | |
ScrollView(.horizontal, showsIndicators: false) { | |
HStack(spacing: 30) { | |
if let hourlyWeather = selectedWeather | |
.hourlyWeather | |
{ | |
ForEach(hourlyWeather, id: \.self.date) | |
{ weatherEntry in | |
VStack(spacing: 8) { | |
Text( | |
formatDate( | |
weatherEntry.date | |
) | |
) | |
Image( | |
systemName: weatherEntry | |
.symbolName | |
) | |
.imageScale(.large) | |
.symbolRenderingMode( | |
.hierarchical | |
) | |
Text( | |
"\(weatherEntry.temperature.description.split(separator: ".").prefix(1).joined())°" | |
) | |
} | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
} | |
} else { | |
ForEach(selectedWeather.persistedHourly) | |
{ snapshot in | |
VStack(spacing: 8) { | |
Text(formatDate(snapshot.date)) | |
Image( | |
systemName: snapshot | |
.symbolName | |
) | |
.imageScale(.large) | |
.symbolRenderingMode( | |
.hierarchical | |
) | |
Text( | |
"\(Int(snapshot.temperatureC.rounded()))°" | |
) | |
} | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
} | |
} | |
} | |
.padding(.horizontal) | |
} | |
.frame(height: 90) | |
.containerRelativeFrame(.vertical) | |
.scrollTargetLayout() | |
.scrollTransition { content, phase in | |
content | |
.opacity(phase.isIdentity ? 1 : 0) | |
.blur(radius: phase.isIdentity ? 0 : 5) | |
} | |
// Daily Weather Strip | |
ScrollView(.horizontal, showsIndicators: false) { | |
HStack(spacing: 30) { | |
if let dailyWeather = selectedWeather | |
.dailyWeather | |
{ | |
ForEach(dailyWeather, id: \.self.date) { | |
weatherEntry in | |
VStack(spacing: 8) { | |
Text( | |
weatherEntry.date.formatted( | |
Date.FormatStyle() | |
.weekday( | |
.abbreviated | |
) | |
) | |
) | |
Image( | |
systemName: weatherEntry | |
.symbolName | |
) | |
.imageScale(.large) | |
.symbolRenderingMode( | |
.hierarchical | |
) | |
Text( | |
"\(weatherEntry.lowTemperature.description.split(separator: ".").prefix(1).joined())°/\(weatherEntry.highTemperature.description.split(separator: ".").prefix(1).joined())°" | |
) | |
} | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
} | |
} else { | |
ForEach(selectedWeather.persistedDaily) | |
{ snapshot in | |
VStack(spacing: 8) { | |
Text( | |
snapshot.date.formatted( | |
Date.FormatStyle() | |
.weekday( | |
.abbreviated | |
) | |
) | |
) | |
Image( | |
systemName: snapshot | |
.symbolName | |
) | |
.imageScale(.large) | |
.symbolRenderingMode( | |
.hierarchical | |
) | |
Text( | |
"\(Int(snapshot.lowTemperatureC.rounded()))°/\(Int(snapshot.highTemperatureC.rounded()))°" | |
) | |
} | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
} | |
} | |
} | |
.padding(.horizontal) | |
} | |
.frame(height: 90) | |
.containerRelativeFrame(.vertical) | |
.scrollTargetLayout() | |
.scrollTransition { content, phase in | |
content | |
.opacity(phase.isIdentity ? 1 : 0) | |
.blur(radius: phase.isIdentity ? 0 : 5) | |
} | |
} | |
} | |
.frame(height: 90) | |
.scrollTargetBehavior(.paging) | |
.safeAreaPadding(.bottom) | |
} | |
.containerRelativeFrame(.vertical) | |
.safeAreaPadding(.bottom) | |
.scrollTargetLayout() | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0.5 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
VStack(spacing: 16) { | |
ForEach(0..<20, id: \.self) { i in | |
if #available(iOS 26.0, *) { | |
Text("Extra content row \(i)") | |
.frame(maxWidth: .infinity) | |
.padding() | |
.background(.ultraThinMaterial,in: RoundedRectangle(cornerRadius: 20)) | |
.colorScheme(selectedWeather.currentWeather?.isDaylight ?? true ? .light :.dark) | |
.scrollTransition { | |
content, | |
phase in | |
content | |
.opacity( | |
phase.isIdentity ? 1 : 0.5 | |
) | |
.blur( | |
radius: phase.isIdentity | |
? 0 : 5 | |
) | |
} | |
} else { | |
// Fallback on earlier versions | |
} | |
} | |
} | |
.padding() | |
} | |
.scrollIndicators(.hidden) | |
.scrollTargetBehavior(.paging) | |
} | |
.toolbar { | |
#if DEBUG | |
ToolbarItem { | |
Button { | |
do { | |
try context.delete(model: WeatherModel.self) | |
print("deleted succesfully") | |
} catch { | |
print("failed to delete") | |
} | |
} label: { | |
Image(systemName: "trash") | |
} | |
} | |
#endif | |
ToolbarItem { | |
Button { | |
viewModel.showLocations.toggle() | |
} label: { | |
Image(systemName: "rectangle.grid.1x2") | |
} | |
} | |
ToolbarItem { | |
Button { | |
viewModel.settingsIsPresented.toggle() | |
} label: { | |
Image(systemName: "gearshape") | |
} | |
} | |
} | |
.redacted(reason: viewModel.isLoading ? .placeholder : []) | |
.foregroundStyle(isDaylight ? .black : .white) | |
.sheet(isPresented: $viewModel.settingsIsPresented) { | |
SettingsView() | |
} | |
.sheet(isPresented: $viewModel.showLocations) { | |
SearchPage(showLocations: $viewModel.showLocations) | |
.environment(viewModel) | |
} | |
} | |
} | |
} | |
@MainActor | |
var contt: ModelContainer { | |
do { | |
let container = try ModelContainer(for: WeatherModel.self) | |
container.mainContext.insert( | |
WeatherModel(location: CLLocation(latitude: 31, longitude: 35)) | |
) | |
return container | |
} catch { | |
fatalError("Could not initialize ModelContainer") | |
} | |
} | |
#Preview { | |
WeatherView() | |
.modelContainer(contt) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment