Files
aris-old/IrisCompanion/iris/ViewModels/CandidatesViewModel.swift

169 lines
7.3 KiB
Swift

//
// 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+."
}
}
}
}
}