From 5c4754caffdaf0ed0c2123cb4813da390c4a4243 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 30 May 2026 13:51:14 +0200 Subject: [PATCH] =?UTF-8?q?Wetter:=20st=C3=BCndliche=20Niederschlags-Timel?= =?UTF-8?q?ine=20(WeatherKit=20hourlyForecast)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Säulen-Diagramm zwischen Stats-Grid und Hunde-Hinweisen: - Heute: nächste 12h ab jetzt - Andere Tage: 06–22h des Tages - Säulenhöhe aus precipitationAmount in mm, Farbgradient aus Intensität (chance + mm). Header zeigt durchschnittliche Regenwahrscheinlichkeit oder 'trocken' wenn < 5%. --- BanYaroGo/Views/WetterView.swift | 94 ++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/BanYaroGo/Views/WetterView.swift b/BanYaroGo/Views/WetterView.swift index 57fd58f..e04aec8 100644 --- a/BanYaroGo/Views/WetterView.swift +++ b/BanYaroGo/Views/WetterView.swift @@ -54,6 +54,7 @@ struct WetterView: View { gassiScoreBadge(metrics: metrics) summaryCard(day: day, metrics: metrics) statsGrid(metrics: metrics) + rainTimeline(weather: weather, day: day, isToday: safeIndex == 0) hundeHinweise(day: day, metrics: metrics) schnueffelCard(metrics: metrics) } @@ -187,6 +188,99 @@ struct WetterView: View { .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) } + // MARK: - Niederschlags-Timeline + + private func rainTimeline(weather: Weather, day: DayWeather, isToday: Bool) -> some View { + let hours = relevantHours(weather: weather, day: day, isToday: isToday) + let maxMm = max(hours.map { $0.precipitationAmount.converted(to: .millimeters).value }.max() ?? 0, 0.5) + let totalChance = hours.reduce(0.0) { $0 + $1.precipitationChance } / Double(max(1, hours.count)) + + return VStack(alignment: .leading, spacing: 10) { + HStack { + Label("Niederschlag", systemImage: "umbrella.fill") + .font(.headline) + .foregroundStyle(.blue) + Spacer() + Text(headerLabel(isToday: isToday, totalChance: totalChance)) + .font(.caption) + .foregroundStyle(.secondary) + } + + if hours.isEmpty { + Text("Keine stündlichen Daten verfügbar.") + .font(.caption) + .foregroundStyle(.secondary) + } else { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .bottom, spacing: 6) { + ForEach(Array(hours.enumerated()), id: \.offset) { _, hour in + rainBar(for: hour, scaleMm: maxMm) + } + } + .frame(height: 110) + } + } + } + .padding(14) + .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) + } + + private func relevantHours(weather: Weather, day: DayWeather, isToday: Bool) -> [HourWeather] { + let all = weather.hourlyForecast + if isToday { + // Ab jetzt die nächsten 12 Stunden + return Array(all.filter { $0.date >= .now }.prefix(12)) + } + // Der ausgewählte Tag, 06:00 bis 22:00 + let cal = Calendar.current + return all + .filter { cal.isDate($0.date, inSameDayAs: day.date) } + .filter { + let h = cal.component(.hour, from: $0.date) + return h >= 6 && h <= 22 + } + } + + private func rainBar(for hour: HourWeather, scaleMm: Double) -> some View { + let chance = hour.precipitationChance + let mm = hour.precipitationAmount.converted(to: .millimeters).value + // Höhe primär aus mm, mindestens 6 (auch bei 0 sichtbarer Bar-Sockel) + let height: CGFloat = max(6, CGFloat(mm / scaleMm) * 50) + let intensity: Double = min(1.0, chance + mm / 5) + let baseColor = Color.blue + return VStack(spacing: 3) { + Text("\(Int((chance * 100).rounded()))%") + .font(.caption2.monospacedDigit()) + .foregroundStyle(chance >= 0.3 ? .primary : .secondary) + Rectangle() + .fill(LinearGradient( + colors: [baseColor.opacity(0.4 + 0.6 * intensity), baseColor.opacity(0.2 + 0.2 * intensity)], + startPoint: .top, + endPoint: .bottom + )) + .frame(width: 22, height: height) + .clipShape(RoundedRectangle(cornerRadius: 4)) + Text(hourLabel(hour.date)) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + .frame(height: 100, alignment: .bottom) + } + + private func hourLabel(_ date: Date) -> String { + let f = DateFormatter() + f.locale = Locale(identifier: "de_DE") + f.dateFormat = "HH" + return "\(f.string(from: date)):00" + } + + private func headerLabel(isToday: Bool, totalChance: Double) -> String { + let pct = Int((totalChance * 100).rounded()) + let prefix = isToday ? "nächste 12 h" : "06–22 h" + if totalChance < 0.05 { return "\(prefix) · trocken" } + return "\(prefix) · ⌀ \(pct) % Regen" + } + // MARK: - Hunde-Hinweise private func hundeHinweise(day: DayWeather, metrics: DayMetrics) -> some View {