← Back to index

WatchBusArrivalsService

Spots Watch App
CodeWhat It DoesHow It Does It
▶ IMPORTS
import Foundation import CoreLocation import Combine import SwiftUIFramework importsImports Foundation, CoreLocation, Combine, SwiftUI.
▶ MODELS
struct WatchNearbyBusStop: Identifiable { let id: String // numeric stop ID, e.g. "308213" let name: String let arrivals: [WatchBusArrival] }`WatchNearbyBusStop` structDefines the `WatchNearbyBusStop` struct. Conforms to Identifiable.
struct WatchBusArrival: Identifiable { let id = UUID() let route: String // e.g. "M15" let headsign: String // e.g. "EAST HARLEM" let seconds: Double var label: String { seconds < 30 ? "Now" : "\(Int(seconds / 60))" } }`WatchBusArrival` structDefines the `WatchBusArrival` struct. Conforms to Identifiable.
▶ SERVICE
@MainActor final class WatchBusArrivalsService: ObservableObject { static let shared = WatchBusArrivalsService() private init() {} @Published private(set) var stops: [WatchNearbyBusStop] = [] @Published private(set) var isLoading = false @Published private(set) var errorMessage: String? = nil private let apiKey = "a0ef7464-ed93-468f-9f98-e26336a1d378" private let baseOBA = "https://bustime.mta.info/api/where" private let baseSIRI = "https://bustime.mta.info/api/siri" // MARK: - Public func refresh(near location: CLLocation) async { isLoading = true errorMessage = nil defer { isLoading = false } // 1. Find nearby stops via OneBusAway stops-for-location let lat = location.coordinate.latitude let lon = location.coordinate.longitude var nearbyStopIDs: [(id: String, name: String, lat: Double, lon: Double)] = [] var obaComps = URLComponents(string: "\(baseOBA)/stops-for-location.json")! obaComps.queryItems = [ URLQueryItem(name: "key", value: apiKey), URLQueryItem(name: "lat", value: "\(lat)"), URLQueryItem(name: "lon", value: "\(lon)"), URLQueryItem(name: "radius", value: "400"), URLQueryItem(name: "maxCount", value: "8"), ] let cfg = URLSessionConfiguration.ephemeral cfg.timeoutIntervalForRequest = 8 cfg.timeoutIntervalForResource = 12 let session = URLSession(configuration: cfg) defer { session.invalidateAndCancel() } if let url = obaComps.url, let (data, _) = try? await session.data(for: URLRequest(url: url, timeoutInterval: 8)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let data2 = json["data"] as? [String: Any], let list = data2["stops"] as? [[String: Any]] { // key is "stops", not "list" for item in list { // "code" is the plain numeric stop ID used by SIRI MonitoringRef guard let code = item["code"] as? String, let name = item["name"] as? String, let sLat = item["lat"] as? Double, let sLon = item["lon"] as? Double else { continue } nearbyStopIDs.append((id: code, name: name, lat: sLat, lon: sLon)) } } // Sort by our own computed distance so order is stable across refreshes. nearbyStopIDs.sort { a, b in let da = (a.lat - lat) * (a.lat - lat) + (a.lon - lon) * (a.lon - lon) let db = (b.lat - lat) * (b.lat - lat) + (b.lon - lon) * (b.lon - lon) return da < db } let topStops = Array(nearbyStopIDs.prefix(4)) if topStops.isEmpty { errorMessage = "No bus stops found nearby" return } // 2. Fetch SIRI stop-monitoring for each stop concurrently let now = Date().timeIntervalSince1970 let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let results: [WatchNearbyBusStop?] = await withTaskGroup(of: WatchNearbyBusStop?.self) { group in for stop in topStops { var siriComps = URLComponents(string: "\(baseSIRI)/stop-monitoring.json")! siriComps.queryItems = [ URLQueryItem(name: "key", value: apiKey), URLQueryItem(name: "OperatorRef", value: "MTA"), URLQueryItem(name: "MonitoringRef", value: stop.id), URLQueryItem(name: "MaximumStopVisits", value: "6"), ] guard let url = siriComps.url else { continue } let stopID = stop.id let stopName = stop.name group.addTask { let req = URLRequest(url: url, timeoutInterval: 8) guard let (data, _) = try? await session.data(for: req), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let siri = json["Siri"] as? [String: Any], let delivery = siri["ServiceDelivery"] as? [String: Any], let smdArr = delivery["StopMonitoringDelivery"] as? [[String: Any]], let smd = smdArr.first, let visits = smd["MonitoredStopVisit"] as? [[String: Any]] else { return nil } var arrivals: [WatchBusArrival] = [] 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) ?? "" 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 } let cleanRoute = route.contains("_") ? String(route.split(separator: "_", maxSplits: 1).last ?? Substring(route)) : route arrivals.append(WatchBusArrival(route: cleanRoute, headsign: headsign, seconds: secs)) } guard !arrivals.isEmpty else { return nil } return WatchNearbyBusStop(id: stopID, name: stopName, arrivals: arrivals) } } var out: [WatchNearbyBusStop?] = [] for await r in group { out.append(r) } return out } // Preserve the OBA distance order let orderedIDs = topStops.map(\.id) stops = results .compactMap { $0 } .sorted { a, b in let ia = orderedIDs.firstIndex(of: a.id) ?? Int.max let ib = orderedIDs.firstIndex(of: b.id) ?? Int.max return ia < ib } if stops.isEmpty { errorMessage = "No buses found nearby" } } }Code blockSee source code for full implementation.
▶ BOROUGH COLOR (MIRRORS IOS BUSROUTECOLOR)
func watchBusRouteColor(for route: String) -> Color { let up = route.uppercased() if up.hasPrefix("BX") { return Color(red: 0/255, green: 147/255, blue: 60/255) } if up.hasPrefix("B") && !up.hasPrefix("BX") { return Color(red: 255/255, green: 99/255, blue: 25/255) } if up.hasPrefix("Q") { return Color(red: 185/255, green: 51/255, blue: 173/255) } if up.hasPrefix("S") { return Color(red: 128/255, green: 129/255, blue: 131/255) } return Color(red: 0/255, green: 57/255, blue: 166/255) // Manhattan / default blue }`watchBusRouteColor()` functionImplements `watchBusRouteColor`. Returns `Color`.