← Back to index

WatchAlertsService

Spots Watch App
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import CombineFramework importsImports Foundation, Combine.
▶ MODEL
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` structDefines the `WatchSubwayAlert` struct. Conforms to Identifiable.
▶ SERVICE
@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 blockSee source code for full implementation.