remindwall / RWPureSwift / Sources / TrafficAPI / TrafficAPIClient.swift
Back to remindwall — Current RemindWall
import CoreLocation
import Dependencies
import DependenciesMacros
import Foundation
import MapKit
/// A stored coordinate pair - matches MonitoredRoute's lat/lon columns so
/// nothing MapKit-typed leaks into the database layer.
public struct RoutePoint: Equatable, Sendable {
public let latitude: Double
public let longitude: Double
public init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
public struct DriveETA: Equatable, Sendable {
public let expectedTravelTime: TimeInterval
public let distanceMeters: Double
public init(expectedTravelTime: TimeInterval, distanceMeters: Double) {
self.expectedTravelTime = expectedTravelTime
self.distanceMeters = distanceMeters
}
/// Whole minutes, rounded UP - "18 min" that arrives in 17:10 is honest,
/// "17 min" that arrives in 17:50 makes you late.
public var expectedMinutes: Int {
Int((expectedTravelTime / 60).rounded(.up))
}
}
public enum TrafficAPIError: Error, Equatable, Sendable {
case noRoute
case network(String)
}
@DependencyClient
public struct TrafficAPIClient: Sendable {
/// Traffic-aware drive time via Apple Maps. No key store - MapKit rides
/// the app's own entitlement, which is the whole reason TR1 picked it.
public var calculateETA: @Sendable (
_ origin: RoutePoint,
_ destination: RoutePoint
) async throws -> DriveETA
}
extension TrafficAPIClient: DependencyKey {
public static var liveValue: Self {
Self(
calculateETA: { origin, destination in
let request = MKDirections.Request()
request.source = MKMapItem(
placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(
latitude: origin.latitude, longitude: origin.longitude
))
)
request.destination = MKMapItem(
placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(
latitude: destination.latitude, longitude: destination.longitude
))
)
request.transportType = .automobile
do {
let response = try await MKDirections(request: request).calculateETA()
return DriveETA(
expectedTravelTime: response.expectedTravelTime,
distanceMeters: response.distance
)
} catch let error as MKError where error.code == .directionsNotFound {
throw TrafficAPIError.noRoute
} catch {
throw TrafficAPIError.network(error.localizedDescription)
}
}
)
}
}
extension TrafficAPIClient: TestDependencyKey {
public static let testValue = Self()
public static var previewValue: Self {
Self(
calculateETA: { _, _ in
DriveETA(expectedTravelTime: 18 * 60, distanceMeters: 9200)
}
)
}
}
extension DependencyValues {
public var trafficAPI: TrafficAPIClient {
get { self[TrafficAPIClient.self] }
set { self[TrafficAPIClient.self] = newValue }
}
}