| struct WatchSubwayAlert: Identifiable {
let id: String
let alertType: String // "Delays", "Planned - Stops Skipped", etc.
let header: String
let affectedRoutes: [String] // sorted
let activePeriods: [(start: TimeInterval, end: TimeInterval?)]
} | `WatchSubwayAlert` struct | Defines the `WatchSubwayAlert` struct. Conforms to Identifiable. |
| @MainActor
final class WatchAlertsService: ObservableObject {
static let shared = WatchAlertsService()
private init() {}
@Published private(set) var alerts: [WatchSubwayAlert] = []
@Published private(set) var isLoading = false
private let endpoint = URL(string:
"https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/camsys%2Fsubway-alerts.json")!
// MARK: - Fetch
func fetchIfNeeded() async {
guard alerts.isEmpty else { return }
await fetch()
}
func fetch() async {
isLoading = true
defer { isLoading = false }
let cfg = URLSessionConfiguration.ephemeral
cfg.timeoutIntervalForRequest = 8
cfg.timeoutIntervalForResource = 12
let session = URLSession(configuration: cfg)
defer { session.invalidateAndCancel() }
guard
let (data, _) = try? await session.data(from: endpoint),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let entities = json["entity"] as? [[String: Any]]
else { return }
let now = Date()
let parsed = entities.compactMap { parse($0) }
alerts = parsed.filter { isActive($0, at: now) }
}
// MARK: - Helpers
private func parse(_ entity: [String: Any]) -> WatchSubwayAlert? {
guard
let id = entity["id"] as? String,
let alert = entity["alert"] as? [String: Any],
let header = translatedText(alert["header_text"]),
!header.isEmpty
else { return nil }
let mercury = alert["transit_realtime.mercury_alert"] as? [String: Any]
let alertType = mercury?["alert_type"] as? String ?? ""
let ie = alert["informed_entity"] as? [[String: Any]] ?? []
let routes = ie.compactMap { $0["route_id"] as? String }
.filter { !$0.isEmpty }
let unique = Array(Set(routes)).sorted()
let periods: [(start: TimeInterval, end: TimeInterval?)] = {
guard let ps = alert["active_period"] as? [[String: Any]] else { return [] }
return ps.compactMap { p in
func ti(_ k: String) -> TimeInterval? {
if let n = p[k] as? TimeInterval { return n }
if let s = p[k] as? String { return TimeInterval(s) }
return nil
}
guard let s = ti("start") else { return nil }
return (start: s, end: ti("end"))
}
}()
return WatchSubwayAlert(id: id, alertType: alertType, header: header,
affectedRoutes: unique, activePeriods: periods)
}
private func isActive(_ alert: WatchSubwayAlert, at date: Date) -> Bool {
guard !alert.activePeriods.isEmpty else { return true }
let t = date.timeIntervalSince1970
return alert.activePeriods.contains { p in
t >= p.start && (p.end == nil || t <= p.end!)
}
}
private func translatedText(_ value: Any?) -> String? {
guard
let dict = value as? [String: Any],
let trans = dict["translation"] as? [[String: Any]]
else { return nil }
let hit = trans.first { ($0["language"] as? String)?.hasPrefix("en") == true }
?? trans.first
return hit?["text"] as? String
}
} | Code block | See source code for full implementation. |