From 3e226ece5754ad4923b3a0962fe7e9e03be8350e Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Wed, 12 Aug 2026 13:07:44 +0100 Subject: [PATCH] Widget links maps:// directly so CarPlay can route to Apple Maps - The widget now uses the native maps://?daddr= scheme for taps in both sizes. In CarPlay there is no containing app to relay through, but since Apple Maps IS a CarPlay app, the system may route the tap straight to Maps on the car display (WWDC25: widgets can launch apps in CarPlay). - iOS keeps working via onOpenURL: the app now forwards maps:// URLs (if the Home Screen delivers them to the app), plus legacy fuelboard:// and http://maps.apple.com links from older cached timelines. - No public API exists for a widget to detect CarPlay context, so the single maps:// link serves both; the app-side forward is the iOS safety net. 35 tests pass. --- FuelBoard/FuelBoardApp.swift | 37 +++++++++++++++++++------- FuelBoardWidgets/FuelPriceWidget.swift | 12 ++++++--- Shared/FuelStore.swift | 15 ++++++----- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/FuelBoard/FuelBoardApp.swift b/FuelBoard/FuelBoardApp.swift index a11e230..4513dd7 100644 --- a/FuelBoard/FuelBoardApp.swift +++ b/FuelBoard/FuelBoardApp.swift @@ -9,18 +9,35 @@ struct FuelBoardApp: App { } } - /// WidgetKit widgets can only open their containing app, so a widget tap - /// delivers a `fuelboard://directions?lat=..&lng=..` URL here instead of - /// opening Apple Maps itself. Forward it to the native Maps directions - /// URL (`maps://?daddr=`), which is what the app's own rows use. + /// Handles deep links that end up in the app. Widget taps arrive here in + /// two cases: + /// - legacy/cached widget timelines using the `fuelboard://` relay, or + /// - the Home Screen delivering a `maps://` URL to the containing app + /// instead of opening Maps directly (the system's choice on iOS). + /// Either way we forward to the native Apple Maps directions URL. The + /// `http://maps.apple.com` form is also handled, from very old timelines. + /// In CarPlay the widget uses the same `maps://` URL, which routes to + /// Apple Maps without ever launching this app. private func handleOpenURL(_ url: URL) { - guard url.scheme == "fuelboard", - url.host == "directions", - let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let lat = components.queryItems?.first(where: { $0.name == "lat" })?.value.flatMap(Double.init), - let lng = components.queryItems?.first(where: { $0.name == "lng" })?.value.flatMap(Double.init), - let mapsURL = URL(string: "maps://?daddr=\(lat),\(lng)&t=d") + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let items = components.queryItems else { return } + + // Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com). + if let daddr = items.first(where: { $0.name == "daddr" })?.value { + openMaps(daddr: daddr) + return + } + + // Legacy relay: fuelboard://directions?lat=..&lng=.. + if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init), + let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) { + openMaps(daddr: "\(lat),\(lng)") + } + } + + private func openMaps(daddr: String) { + guard let mapsURL = URL(string: "maps://?daddr=\(daddr)&t=d") else { return } UIApplication.shared.open(mapsURL) } } diff --git a/FuelBoardWidgets/FuelPriceWidget.swift b/FuelBoardWidgets/FuelPriceWidget.swift index 36d4068..c8b3bce 100644 --- a/FuelBoardWidgets/FuelPriceWidget.swift +++ b/FuelBoardWidgets/FuelPriceWidget.swift @@ -5,9 +5,13 @@ import AppIntents // FuelBoard widget — petrol stations near you. // systemMedium: top 3-4 stations with price + distance, each row opens Maps // systemSmall: single station, whole widget opens Maps -// Taps deep-link to Apple Maps directions (maps://?daddr=). -// Note: on the home screen Link opens Maps directly. Inside CarPlay the widget -// renders but can't launch Maps (widgets can only launch their own CarPlay app). +// Taps deep-link to Apple Maps directions (maps://?daddr=). The widget uses +// the native Maps scheme (not a custom app scheme) so that CarPlay can route +// the tap straight to Apple Maps — which IS a CarPlay app — without needing +// the containing app to launch. On the Home Screen the system either opens +// Maps directly or delivers the URL to FuelBoard, whose onOpenURL forwards it +// (and also still handles legacy fuelboard:// and http://maps.apple.com links +// from older cached timelines). // // Each widget instance is configured INDEPENDENTLY via its own App Intent // (long-press → Edit Widget): fuel type (Unleaded/Premium/Diesel) and sort @@ -261,7 +265,7 @@ struct FuelPriceWidgetView: View { .foregroundStyle(.secondary) } ForEach(entry.stations.prefix(3)) { station in - Link(destination: station.widgetDirectionsURL ?? URL(string: "fuelboard://")!) { + Link(destination: station.widgetDirectionsURL ?? URL(string: "maps://")!) { HStack(spacing: 8) { Text(station.name) .font(.caption.weight(.semibold)) diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index 3879ea4..c3de72e 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -159,13 +159,16 @@ struct FuelStation: Identifiable, Codable, Equatable { URL(string: "maps://?daddr=\(lat),\(lng)&t=d") } - /// Widget tap URL. WidgetKit widgets can ONLY open their containing app: - /// the system hands this URL to FuelBoard via `onOpenURL`, which then - /// forwards to Apple Maps. So the widget link uses our own registered - /// `fuelboard://` scheme rather than `maps://` (which would arrive in the - /// app and be silently dropped). + /// Widget tap URL — the native Maps scheme (`maps://`). From the iOS Home + /// Screen the system may either (a) open Maps directly, or (b) deliver the + /// URL to the containing app, where FuelBoardApp.onOpenURL forwards it to + /// Maps. In CarPlay there is no containing app, but because `maps://` + /// targets Apple Maps — an app that IS in CarPlay — the system may route + /// the tap straight to the car display, bypassing the app entirely. This + /// is the only scheme with a chance of working in CarPlay; the old + /// `fuelboard://` relay is kept as a handled fallback in onOpenURL. var widgetDirectionsURL: URL? { - URL(string: "fuelboard://directions?lat=\(lat)&lng=\(lng)") + URL(string: "maps://?daddr=\(lat),\(lng)&t=d") } /// Name of the bundled brand logo asset, or nil if unknown.