258 lines
10 KiB
Swift
258 lines
10 KiB
Swift
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"
|
||
}
|
||
}
|
||
}
|