mcritz/sftransitkit
A Swift package for accessing San Francisco transit data from the 511.org API.
Overview
SFTransitKit provides a simple and efficient way to access transit data for San Francisco, including:
- Transit lines (routes)
- Stops for each line
- Real-time arrival forecasts
The library handles API communication, data parsing, and provides a caching layer for improved performance.
Requirements
- Swift 6.1+
- macOS 13+, iOS 16+, watchOS 9+, or visionOS 1+
- An API key from 511.org/open-data/transit
Installation
Swift Package Manager
Add SFTransitKit to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/mcritz/SFTransitKit.git", from: "1.0.0")
]API Usage
Initialization
First, initialize the TransitSFAPI with your 511.org API key:
import SFTransitKit
// Initialize with your API key
let api = TransitSFAPI(apiKey: "your-511-api-key")
// For a basic implemetation that includes caching, use `SFTransitService`
let service = SFTransitService(api: api)Extensibility
You can create any type that conforms to the TransitService protocol to build whatever additional features you want, like adding data persistance, further protocol conformances, @Observable, etc.
Fetching Transit Lines
// Fetch all lines for SF Muni (default operator)
do {
let lines = try await service.fetchLines()
for line in lines {
print("Line: \(line.name) (\(line.id))")
}
} catch {
print("Error fetching lines: \(error)")
}
// Fetch lines for a different operator
let acTransitLines = try await service.fetchLines("AC")Fetching Stops for a Line
// Fetch stops for the N-Judah line
do {
let stops = try await service.fetchStops("N")
for stop in stops {
print("Stop: \(stop.name) at \(stop.location.latitude), \(stop.location.longitude)")
}
} catch {
print("Error fetching stops: \(error)")
}
// Fetch stops for a specific line and operator
let jLineStops = try await service.fetchStops("SF", line: "J")Getting Real-time Arrival Forecasts
// Get real-time forecasts for a specific stop
let stopID = "14510" // Example stop ID
let result = await service.fetchRealtime(stopID)
switch result {
case .success(let forecasts):
for forecast in forecasts {
print("\(forecast.destination ?? "Unknown destination"): arriving in \(forecast.waitFormatted)")
}
case .failure(let error):
print("Error fetching real-time data: \(error)")
}Testing
SFTransitKit is designed with testability in mind and includes several components to facilitate testing.
Using the Mock Network Client
For testing without making actual API calls, use the MockNetworkClient:
#if DEBUG
import XCTest
@testable import SFTransitKit
func testYourFeature() async throws {
// Create a mock network client
let mockClient = MockNetworkClient()
// Create test data
let testData = """
{
"your": "test data here"
}
""".data(using: .utf8)!
// Configure the mock to return your test data
let api = API(apiKey: "test-api-key", networkClient: mockClient)
let url = API.Endpoint.lines(operatorCode: "SF").url("test-api-key")
await mockClient.updateResponseData([url: testData])
// Test your code that uses the API
// ...
}
#endifUsing Fixture Data
The package includes a helper method to load fixture data for tests:
#if DEBUG
// Load fixture data
let mockClient = MockNetworkClient()
let fixtureData = try mockClient.loadFixture(named: "lines.json", bundle: .module)
// Use the fixture data in your tests
await mockClient.updateResponseData([url: fixtureData])
#endifUsing the Mock Transit Repository
For higher-level testing, you can use the MockTransitService:
#if DEBUG
// Create a mock repository
let mockRepository = MockTransitService()
// Configure the mock repository
let mockLine = Line(id: "1", name: "Test Line", fromDate: Date(), toDate: Date().addingTimeInterval(86400),
transportMode: .bus, publicCode: "1", siriLineRef: "1", monitored: true, operatorRef: "SF")
await mockRepository.updateLinesResult(.success([mockLine]))
// Test code that uses the repository
let lines = try await mockRepository.fetchLines()
XCTAssertEqual(lines.count, 1)
XCTAssertEqual(lines[0].id, "1")
#endifError Handling
SFTransitKit uses Swift's error handling mechanisms. Network and parsing errors are propagated to the caller:
do {
let lines = try await repository.fetchLines()
// Process lines
} catch {
if let urlError = error as? URLError {
// Handle network errors
switch urlError.code {
case .notConnectedToInternet:
// Handle no internet connection
break
default:
// Handle other URL errors
break
}
} else {
// Handle other errors (like parsing errors)
}
}For real-time data, the API returns a Result type:
let result = await repository.fetchRealtime(stopID)
switch result {
case .success(let forecasts):
// Process forecasts
case .failure(let error):
// Handle error
}Package Metadata
Repository: mcritz/sftransitkit
Default branch: main
README: README.md