Refactor data sources and feed model
This commit is contained in:
@@ -11,7 +11,7 @@ import os
|
||||
|
||||
@MainActor
|
||||
final class CandidatesViewModel: ObservableObject {
|
||||
@Published private(set) var candidates: [Candidate] = []
|
||||
@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
|
||||
@@ -41,24 +41,121 @@ final class CandidatesViewModel: ObservableObject {
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
let ds = WeatherDataSource()
|
||||
let result = await ds.candidatesWithDiagnostics(for: location, now: now)
|
||||
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 = result.candidates.sorted { $0.confidence > $1.confidence }
|
||||
self.diagnostics = result.diagnostics
|
||||
if let error = result.weatherKitError {
|
||||
self.candidates = items
|
||||
self.diagnostics = output.diagnostics
|
||||
if let error = output.diagnostics["weatherkit_error"] {
|
||||
self.lastError = "WeatherKit error: \(error)"
|
||||
}
|
||||
}
|
||||
|
||||
if let error = result.weatherKitError {
|
||||
if let error = output.diagnostics["weatherkit_error"] {
|
||||
self.logger.error("WeatherKit error: \(error)")
|
||||
}
|
||||
self.logger.info("Produced candidates count=\(result.candidates.count)")
|
||||
for c in result.candidates {
|
||||
self.logger.info("Candidate id=\(c.id, privacy: .public) type=\(c.type.rawValue, privacy: .public) conf=\(c.confidence, format: .fixed(precision: 2)) ttl=\(c.ttlSec) title=\(c.title, privacy: .public)")
|
||||
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 result.candidates.isEmpty {
|
||||
self.logger.info("Diagnostics: \(String(describing: result.diagnostics), privacy: .public)")
|
||||
if items.isEmpty {
|
||||
self.logger.info("Diagnostics: \(String(describing: output.diagnostics), privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
|
||||
Reference in New Issue
Block a user