Initial commit
This commit is contained in:
257
WhiteNights/Widgets/WeatherView.swift
Normal file
257
WhiteNights/Widgets/WeatherView.swift
Normal file
@ -0,0 +1,257 @@
|
||||
import SwiftUI
|
||||
|
||||
private let WEATHER_STATUS_MAP: [String: String] = [
|
||||
"Rain": "дождливо",
|
||||
"Clouds": "облачно",
|
||||
"Clear": "солнечно",
|
||||
"Thunderstorm": "гроза",
|
||||
"Snow": "снег",
|
||||
"Drizzle": "мелкий дождь",
|
||||
"Fog": "туман"
|
||||
]
|
||||
|
||||
struct FormattedWeather {
|
||||
let temperature: Int
|
||||
let status: String
|
||||
let precipitation: Int?
|
||||
let windSpeed: Double?
|
||||
let dayOfWeek: String?
|
||||
}
|
||||
|
||||
struct WeatherView: View {
|
||||
@State private var todayWeather: FormattedWeather?
|
||||
@State private var forecast: [FormattedWeather] = []
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
if let today = todayWeather {
|
||||
// ЛЕВЫЙ СТОЛБЕЦ: Иконка, температура и статус
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Image(getWeatherIconName(for: today.status))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("\(today.temperature)°")
|
||||
.font(.system(size: 30, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(today.status)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// ПРАВЫЙ СТОЛБЕЦ: Прогноз с иконками
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// Прогноз на 3 дня
|
||||
ForEach(forecast.prefix(3).indices, id: \.self) { index in
|
||||
let day = forecast[index]
|
||||
HStack(spacing: 5) {
|
||||
Image(getWeatherIconName(for: day.status))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
|
||||
if let dayName = day.dayOfWeek {
|
||||
Text(dayName)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text("\(day.temperature)°")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
}
|
||||
|
||||
// Влажность и скорость ветра
|
||||
if let precipitation = today.precipitation {
|
||||
HStack(spacing: 5) {
|
||||
Image("det_humidity")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
Text("\(precipitation)%")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
if let windSpeed = today.windSpeed {
|
||||
HStack(spacing: 5) {
|
||||
Image("det_wind_speed")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
Text("\(Int(windSpeed)) м/с")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Загрузка погоды...")
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.background(
|
||||
ZStack {
|
||||
Color(hex: 0x806C59)
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0.0), location: 0.0871),
|
||||
.init(color: .white.opacity(0.16), location: 0.6969)
|
||||
],
|
||||
startPoint: .bottomLeading,
|
||||
endPoint: .topTrailing
|
||||
)
|
||||
}
|
||||
)
|
||||
.cornerRadius(25)
|
||||
.shadow(color: Color.black.opacity(0.10), radius: 8, x: 0, y: 2)
|
||||
.task {
|
||||
await fetchAndFormatWeather()
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAndFormatWeather() async {
|
||||
let lat = 59.938784
|
||||
let lng = 30.314997
|
||||
|
||||
guard let url = URL(string: "https://white-nights.krbl.ru/services/weather") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let parameters: [String: Any] = [
|
||||
"coordinates": ["latitude": lat, "longitude": lng]
|
||||
]
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// 💡 Отладка: Печатаем полученные данные для проверки
|
||||
// print("Received Data (String): \(String(data: data, encoding: .utf8) ?? "N/A")")
|
||||
|
||||
let weatherResponse = try JSONDecoder().decode(WeatherResponse.self, from: data)
|
||||
|
||||
formatWeatherData(data: weatherResponse)
|
||||
|
||||
} catch {
|
||||
print("Ошибка загрузки погоды: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func formatWeatherData(data: WeatherResponse) {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // UTC
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Добавление POSIX-локали для надежного парсинга
|
||||
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = TimeZone(secondsFromGMT: 0)! // UTC
|
||||
|
||||
// 1. Фильтруем все записи, которые соответствуют 12:00 UTC
|
||||
let middayForecast = data.forecast.filter { item in
|
||||
guard let date = dateFormatter.date(from: item.date) else {
|
||||
// 💡 Отладка: если здесь вы увидите сообщения, значит, формат даты API не совпадает с форматом dateFormatter
|
||||
// print("Парсинг даты провален для: \(item.date)")
|
||||
return false
|
||||
}
|
||||
return calendar.component(.hour, from: date) == 12
|
||||
}
|
||||
|
||||
// 💡 Отладка: Проверяем, сколько записей найдено
|
||||
// print("Найдено записей на 12:00 UTC: \(middayForecast.count)")
|
||||
|
||||
// Сегодня
|
||||
if let today = data.currentWeather {
|
||||
self.todayWeather = FormattedWeather(
|
||||
temperature: Int(today.temperatureCelsius.rounded()),
|
||||
status: WEATHER_STATUS_MAP[today.description] ?? today.description,
|
||||
precipitation: today.humidity,
|
||||
windSpeed: today.windSpeed,
|
||||
dayOfWeek: nil
|
||||
)
|
||||
}
|
||||
|
||||
// 🚀 ИСПРАВЛЕНИЕ: Прогноз на 3 дня. Берем первые 3 доступные записи с 12:00.
|
||||
var formattedForecast: [FormattedWeather] = []
|
||||
|
||||
// Перебираем первые 3 записи с 12:00 UTC
|
||||
for item in middayForecast.prefix(3) {
|
||||
guard let date = dateFormatter.date(from: item.date) else { continue }
|
||||
|
||||
let averageTemp = (item.minTemperatureCelsius + item.maxTemperatureCelsius) / 2
|
||||
let dayOfWeekString = getDayOfWeek(from: date)
|
||||
|
||||
// 💡 Отладка: Печатаем найденный прогноз
|
||||
// print("Прогноз найден: \(dayOfWeekString) - \(Int(averageTemp.rounded()))°")
|
||||
|
||||
formattedForecast.append(FormattedWeather(
|
||||
temperature: Int(averageTemp.rounded()),
|
||||
status: WEATHER_STATUS_MAP[item.description] ?? item.description,
|
||||
precipitation: item.humidity,
|
||||
windSpeed: item.windSpeed,
|
||||
dayOfWeek: dayOfWeekString
|
||||
))
|
||||
}
|
||||
|
||||
// Если данных меньше 3, заполняем оставшиеся места нулями (N/A)
|
||||
let daysToFill = 3 - formattedForecast.count
|
||||
if daysToFill > 0 {
|
||||
let baseDate = Date()
|
||||
|
||||
for i in 1...daysToFill {
|
||||
guard let nextDayForNA = Calendar.current.date(byAdding: .day, value: formattedForecast.count + i, to: baseDate) else { continue }
|
||||
|
||||
let dayOfWeekString = getDayOfWeek(from: nextDayForNA)
|
||||
|
||||
formattedForecast.append(FormattedWeather(
|
||||
temperature: 0,
|
||||
status: "N/A",
|
||||
precipitation: nil,
|
||||
windSpeed: nil,
|
||||
dayOfWeek: dayOfWeekString
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
self.forecast = formattedForecast
|
||||
}
|
||||
|
||||
private func getDayOfWeek(from date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "ru_RU")
|
||||
formatter.dateFormat = "E"
|
||||
return formatter.string(from: date).capitalized
|
||||
}
|
||||
|
||||
private func getWeatherIconName(for status: String) -> String {
|
||||
let normalizedStatus = status.lowercased()
|
||||
|
||||
switch normalizedStatus {
|
||||
case "солнечно":
|
||||
return "cond_sunny"
|
||||
case "облачно":
|
||||
return "cond_cloudy"
|
||||
case "дождливо", "мелкий дождь":
|
||||
return "cond_rainy"
|
||||
case "снег":
|
||||
return "cond_snowy"
|
||||
case "гроза":
|
||||
return "cond_thunder"
|
||||
case "туман":
|
||||
return "det_humidity"
|
||||
default:
|
||||
return "cond_sunny"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user