// // CandidatesViewModel.swift // iris // // Created by Codex. // import CoreLocation import Foundation import os @MainActor final class CandidatesViewModel: ObservableObject { @Published private(set) var candidates: [FeedItem] = [] @Published private(set) var lastUpdatedAt: Date? = nil @Published private(set) var isLoading = false @Published private(set) var lastError: String? = nil @Published private(set) var diagnostics: [String: String] = [:] var demoLatitude: Double = 51.5074 var demoLongitude: Double = -0.1278 private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "iris", category: "CandidatesViewModel") func refresh() { guard !isLoading else { return } isLoading = true lastError = nil diagnostics = [:] let location = CLLocation(latitude: demoLatitude, longitude: demoLongitude) let now = Int(Date().timeIntervalSince1970) logger.info("Refresh start lat=\(self.demoLatitude, format: .fixed(precision: 4)) lon=\(self.demoLongitude, format: .fixed(precision: 4)) now=\(now)") Task { defer { Task { @MainActor in self.isLoading = false self.lastUpdatedAt = Date() } } if #available(iOS 16.0, *) { let ds = WeatherDataSource() let output: WeatherDataSource.Snapshot do { output = try await ds.dataWithDiagnostics(for: location, now: now) } catch let error as WeatherDataSource.WeatherError { switch error { case .weatherKitFailed(_, let diagnostics): await MainActor.run { self.candidates = [] self.diagnostics = diagnostics self.lastError = "WeatherKit error: \(error.localizedDescription)" } return } } catch { await MainActor.run { self.candidates = [] self.diagnostics = ["weatherkit_error": String(describing: error)] self.lastError = "WeatherKit error: \(error)" } return } let data = output.data var items: [FeedItem] = [] if let current = data.current { items.append( FeedItem( id: "wx:now:\(now / 60)", type: .currentWeather, title: "Now \(current.temperatureC)°C \(current.condition.description)".truncated(maxLength: TextConstraints.titleMax), subtitle: "Feels \(current.feelsLikeC)°C".truncated(maxLength: TextConstraints.subtitleMax), priority: 0.8, ttlSec: 1800, condition: current.condition, startsAt: nil, bucket: .fyi, actions: ["DISMISS"] ) ) } if let rainSoon = data.rainSoon { let minutes = max(0, Int(((TimeInterval(rainSoon.startAt - now)) / 60.0).rounded())) let title = (minutes <= 0) ? "Rain now" : "Rain in ~\(minutes) min" let subtitle = (rainSoon.source == .minutely) ? "Carry an umbrella" : "Rain likely soon" let confidence: Double = (rainSoon.source == .minutely) ? 0.9 : 0.6 items.append( FeedItem( id: "wx:rain:\(rainSoon.startAt)", type: .weatherAlert, title: title.truncated(maxLength: TextConstraints.titleMax), subtitle: subtitle.truncated(maxLength: TextConstraints.subtitleMax), priority: confidence, ttlSec: max(1, rainSoon.ttlSec), condition: nil, startsAt: nil, bucket: .rightNow, actions: ["DISMISS"] ) ) } if let wind = data.windAlert { let mph = Int((wind.gustMps * 2.236936).rounded()) items.append( FeedItem( id: "wx:wind:\(now):\(Int(wind.thresholdMps * 10))", type: .weatherAlert, title: "Wind gusts ~\(mph) mph".truncated(maxLength: TextConstraints.titleMax), subtitle: "Use caution outside".truncated(maxLength: TextConstraints.subtitleMax), priority: 0.8, ttlSec: max(1, wind.ttlSec), condition: nil, startsAt: nil, bucket: .rightNow, actions: ["DISMISS"] ) ) } for warning in data.warnings { items.append( FeedItem( id: warning.id, type: .weatherWarning, title: warning.title.truncated(maxLength: TextConstraints.titleMax), subtitle: warning.subtitle.truncated(maxLength: TextConstraints.subtitleMax), priority: min(max(warning.confidence, 0.0), 1.0), ttlSec: max(1, warning.ttlSec), condition: nil, startsAt: nil, bucket: .rightNow, actions: ["DISMISS"] ) ) } items.sort { $0.priority > $1.priority } await MainActor.run { self.candidates = items self.diagnostics = output.diagnostics if let error = output.diagnostics["weatherkit_error"] { self.lastError = "WeatherKit error: \(error)" } } if let error = output.diagnostics["weatherkit_error"] { self.logger.error("WeatherKit error: \(error)") } self.logger.info("Produced feed items count=\(items.count)") for item in items { self.logger.info("FeedItem id=\(item.id, privacy: .public) type=\(item.type.rawValue, privacy: .public) prio=\(item.priority, format: .fixed(precision: 2)) ttl=\(item.ttlSec) title=\(item.title, privacy: .public)") } if items.isEmpty { self.logger.info("Diagnostics: \(String(describing: output.diagnostics), privacy: .public)") } } else { await MainActor.run { self.candidates = [] self.lastError = "WeatherKit requires iOS 16+." } } } } }