Skip to content

Instantly share code, notes, and snippets.

@MaherSafadii
Created September 5, 2025 19:14
Show Gist options
  • Save MaherSafadii/4b28ba557f9e51051ac29645ca17b952 to your computer and use it in GitHub Desktop.
Save MaherSafadii/4b28ba557f9e51051ac29645ca17b952 to your computer and use it in GitHub Desktop.
//
// 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