← Back to index

BusArrivalsService

Spots
CodeWhat It DoesHow It Does It
▶ IMPORTS
import FoundationFramework importsImports Foundation.
▶ MODEL
struct BusArrivalInfo: Identifiable { let id = UUID() let route: String let headsign: String let seconds: Double // next bus (negative → already there / "at stop") let secondSeconds: Double? let thirdSeconds: Double? var minuteShort: String { Self.format(seconds) } var secondMinuteShort: String? { secondSeconds.map { Self.format($0) } } var thirdMinuteShort: String? { thirdSeconds.map { Self.format($0) } } private static func format(_ secs: Double) -> String { secs < 30 ? "Now" : "\(Int(secs / 60))" } }`BusArrivalInfo` structDefines the `BusArrivalInfo` struct. Conforms to Identifiable.
▶ SERVICE
struct BusArrivalsService { /// Free key from https://register.developer.bus.mta.info static var apiKey: String = "a0ef7464-ed93-468f-9f98-e26336a1d378" private static let baseURL = "https://bustime.mta.info/api/siri/stop-monitoring.json" /// Returns empty array when no key is configured or on any network error. static func fetch(for stop: BusStop) async -> [BusArrivalInfo] { guard !apiKey.isEmpty else { return [] } var comps = URLComponents(string: baseURL)! comps.queryItems = [ URLQueryItem(name: "key", value: apiKey), URLQueryItem(name: "OperatorRef", value: "MTA"), URLQueryItem(name: "MonitoringRef", value: stop.id), URLQueryItem(name: "MaximumStopVisits", value: "9"), ] guard let url = comps.url else { return [] } let cfg = URLSessionConfiguration.ephemeral cfg.timeoutIntervalForRequest = 8 cfg.timeoutIntervalForResource = 10 let session = URLSession(configuration: cfg) defer { session.invalidateAndCancel() } guard let (data, _) = try? await session.data(for: URLRequest(url: url, timeoutInterval: 8)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [] } // Walk the SIRI response tree guard let siri = json["Siri"] as? [String: Any], let delivery = siri["ServiceDelivery"] as? [String: Any], let smdArray = delivery["StopMonitoringDelivery"] as? [[String: Any]], let smd = smdArray.first, let visits = smd["MonitoredStopVisit"] as? [[String: Any]] else { return [] } let now = Date().timeIntervalSince1970 // BusTime timestamps include fractional seconds, e.g. "2026-03-31T23:22:32.177-04:00" // ISO8601DateFormatter must opt-in to fractional seconds or the parse returns nil. let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] var raw: [(route: String, headsign: String, secs: Double)] = [] for visit in visits { guard let mvj = visit["MonitoredVehicleJourney"] as? [String: Any], let route = (mvj["PublishedLineName"] as? String) ?? (mvj["LineRef"] as? String) else { continue } let headsign = (mvj["DestinationName"] as? String) ?? "" // Prefer ExpectedArrivalTime; fall back to AimedArrivalTime let call = mvj["MonitoredCall"] as? [String: Any] let timeStr = (call?["ExpectedArrivalTime"] as? String) ?? (call?["AimedArrivalTime"] as? String) ?? "" guard let date = isoFormatter.date(from: timeStr) else { continue } let secs = date.timeIntervalSince1970 - now guard secs >= -60 else { continue } // ignore buses that left > 1 min ago // Clean up route label: strip "MTA NYCT_" or "MTA_" prefix let cleanRoute: String if let under = route.firstIndex(of: "_") { cleanRoute = String(route[route.index(after: under)...]) } else { cleanRoute = route } raw.append((route: cleanRoute, headsign: headsign, secs: secs)) } // Group by route+headsign, keep next 3 arrival times raw.sort { $0.secs < $1.secs } var first = [String: Double]() var second = [String: Double]() var third = [String: Double]() var order = [String]() for item in raw { let key = "\(item.route)|\(item.headsign)" if first[key] == nil { first[key] = item.secs order.append(key) } else if second[key] == nil { second[key] = item.secs } else if third[key] == nil { third[key] = item.secs } } return order.compactMap { key -> BusArrivalInfo? in guard let secs = first[key] else { return nil } let parts = key.split(separator: "|", maxSplits: 1) let route = parts.count > 0 ? String(parts[0]) : key let headsign = parts.count > 1 ? String(parts[1]) : "" return BusArrivalInfo(route: route, headsign: headsign, seconds: secs, secondSeconds: second[key], thirdSeconds: third[key]) } } }`BusArrivalsService` structDefines the `BusArrivalsService` struct.