Siri: Directions to Cheapest [fuel] AppShortcuts — OpenURLIntent hands off to Apple Maps (universal link)
This commit is contained in:
@@ -125,6 +125,67 @@ struct CheapestFuelIntent: AppIntent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Directions intent
|
||||||
|
|
||||||
|
/// Opens Apple Maps with turn-by-turn directions to the cheapest station
|
||||||
|
/// selling `fuel` within the saved search radius. Shares the exact same
|
||||||
|
/// radius-scoped lookup as `CheapestFuelIntent`; the only difference is the
|
||||||
|
/// result — it hands off to Maps instead of speaking the price.
|
||||||
|
struct DirectionsToCheapestFuelIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Directions to Cheapest Fuel Near Me"
|
||||||
|
static var description = IntentDescription(
|
||||||
|
"Opens Apple Maps directions to the cheapest station selling a fuel near you, using the latest cached prices."
|
||||||
|
)
|
||||||
|
|
||||||
|
@Parameter(title: "Fuel")
|
||||||
|
var fuel: FuelType
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.fuel = .e10
|
||||||
|
}
|
||||||
|
|
||||||
|
init(fuel: FuelType) {
|
||||||
|
self.fuel = fuel
|
||||||
|
}
|
||||||
|
|
||||||
|
func perform() async throws -> some IntentResult & OpensIntent {
|
||||||
|
let stations = FuelStore.loadStations()
|
||||||
|
|
||||||
|
guard !stations.isEmpty else {
|
||||||
|
throw DirectionsError("I don't have any price data yet. Open FuelBoard once to download the latest prices, then ask me again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let coordinate = FuelStore.loadLocationWithDate()?.coordinate else {
|
||||||
|
throw DirectionsError("I don't know your location yet. Open FuelBoard and allow location access, then ask me again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let radiusMiles = Double(FuelStore.loadStationLimit())
|
||||||
|
guard let station = SiriCheapestLookup.cheapest(
|
||||||
|
in: stations,
|
||||||
|
fuel: fuel,
|
||||||
|
fromLat: coordinate.lat,
|
||||||
|
lng: coordinate.lng,
|
||||||
|
withinMiles: radiusMiles
|
||||||
|
) else {
|
||||||
|
let fuelName = fuel.displayName.lowercased()
|
||||||
|
let radiusText = radiusMiles == 1 ? "1 mile" : "\(Int(radiusMiles)) miles"
|
||||||
|
throw DirectionsError("I couldn't find any station selling \(fuelName) within \(radiusText) of you.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = SiriCheapestLookup.mapsURL(latitude: station.lat, longitude: station.lng)
|
||||||
|
return .result(opensIntent: OpenURLIntent(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Localized error thrown when directions can't be produced (no data, no
|
||||||
|
/// location, or no station within the radius). Siri presents the message
|
||||||
|
/// directly.
|
||||||
|
private struct DirectionsError: LocalizedError {
|
||||||
|
let message: String
|
||||||
|
init(_ message: String) { self.message = message }
|
||||||
|
var errorDescription: String? { message }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Snippets
|
// MARK: - Snippets
|
||||||
|
|
||||||
/// Price card shown in Shortcuts / Siri results.
|
/// Price card shown in Shortcuts / Siri results.
|
||||||
@@ -227,5 +288,48 @@ struct FuelBoardShortcuts: AppShortcutsProvider {
|
|||||||
shortTitle: "Cheapest Diesel",
|
shortTitle: "Cheapest Diesel",
|
||||||
systemImageName: "fuelpump"
|
systemImageName: "fuelpump"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Directions — separate intent so the price queries stay answer-only
|
||||||
|
// and only the explicit directions query hands off to Apple Maps.
|
||||||
|
// Phrases use direction verbs ("directions to", "get directions") to
|
||||||
|
// stay distinct from the price shortcut phrases.
|
||||||
|
AppShortcut(
|
||||||
|
intent: DirectionsToCheapestFuelIntent(),
|
||||||
|
phrases: [
|
||||||
|
"Ask \(.applicationName) for directions to the cheapest \(\.$fuel) near me",
|
||||||
|
],
|
||||||
|
shortTitle: "Directions to Cheapest Fuel",
|
||||||
|
systemImageName: "map"
|
||||||
|
)
|
||||||
|
|
||||||
|
AppShortcut(
|
||||||
|
intent: DirectionsToCheapestFuelIntent(fuel: .e10),
|
||||||
|
phrases: [
|
||||||
|
"Ask \(.applicationName) for directions to the cheapest unleaded near me",
|
||||||
|
"Get directions to the cheapest petrol near me \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Directions to Cheapest Unleaded",
|
||||||
|
systemImageName: "map"
|
||||||
|
)
|
||||||
|
|
||||||
|
AppShortcut(
|
||||||
|
intent: DirectionsToCheapestFuelIntent(fuel: .e5),
|
||||||
|
phrases: [
|
||||||
|
"Ask \(.applicationName) for directions to the cheapest premium near me",
|
||||||
|
"Get directions to the cheapest super unleaded near me \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Directions to Cheapest Premium",
|
||||||
|
systemImageName: "map"
|
||||||
|
)
|
||||||
|
|
||||||
|
AppShortcut(
|
||||||
|
intent: DirectionsToCheapestFuelIntent(fuel: .diesel),
|
||||||
|
phrases: [
|
||||||
|
"Ask \(.applicationName) for directions to the cheapest diesel near me",
|
||||||
|
"Get directions to the cheapest diesel near me \(.applicationName)",
|
||||||
|
],
|
||||||
|
shortTitle: "Directions to Cheapest Diesel",
|
||||||
|
systemImageName: "map"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,6 +470,21 @@ final class SiriCheapestLookupTests: XCTestCase {
|
|||||||
XCTAssertNil(SiriCheapestLookup.cheapest(in: [], fuel: .e10, fromLat: 53.01, lng: -1.01, withinMiles: 5))
|
XCTAssertNil(SiriCheapestLookup.cheapest(in: [], fuel: .e10, fromLat: 53.01, lng: -1.01, withinMiles: 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testMapsURLIsUniversalAppleMapsLink() {
|
||||||
|
let url = SiriCheapestLookup.mapsURL(latitude: 53.7538, longitude: -1.8177)
|
||||||
|
XCTAssertEqual(url.scheme, "https")
|
||||||
|
XCTAssertEqual(url.host, "maps.apple.com")
|
||||||
|
XCTAssertEqual(url.query, "daddr=53.7538,-1.8177")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMapsURLUsesUniversalLinkNotCustomScheme() {
|
||||||
|
// OpenURLIntent only opens universal links — a maps:// custom scheme
|
||||||
|
// would be rejected at runtime, so the https form is mandatory.
|
||||||
|
let url = SiriCheapestLookup.mapsURL(latitude: 0, longitude: 0)
|
||||||
|
XCTAssertEqual(url.scheme, "https", "custom schemes like maps:// must not be used")
|
||||||
|
XCTAssertTrue(url.absoluteString.hasPrefix("https://maps.apple.com/"))
|
||||||
|
}
|
||||||
|
|
||||||
func testFreshnessLabelUsesDataUpdatedStamp() {
|
func testFreshnessLabelUsesDataUpdatedStamp() {
|
||||||
let label = SiriCheapestLookup.freshnessLabel(
|
let label = SiriCheapestLookup.freshnessLabel(
|
||||||
updated: "2026-08-14T10:31:36.000Z",
|
updated: "2026-08-14T10:31:36.000Z",
|
||||||
|
|||||||
@@ -59,4 +59,12 @@ enum SiriCheapestLookup {
|
|||||||
plain.formatOptions = [.withInternetDateTime]
|
plain.formatOptions = [.withInternetDateTime]
|
||||||
return plain.date(from: string)
|
return plain.date(from: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apple Maps URL for turn-by-turn directions to a point. MUST be the
|
||||||
|
/// universal-link form (https://maps.apple.com) — OpenURLIntent rejects
|
||||||
|
/// custom schemes like `maps://`, and the https form survives copy/paste
|
||||||
|
/// and the share sheet.
|
||||||
|
static func mapsURL(latitude: Double, longitude: Double) -> URL {
|
||||||
|
URL(string: "https://maps.apple.com/?daddr=\(latitude),\(longitude)")!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user