Widgets: merge small+medium into one kind (both sizes, one gallery entry)

This commit is contained in:
FuelBoard Contributor
2026-08-14 09:52:50 +01:00
parent e62177ae2e
commit 3dd567a5cc
3 changed files with 20 additions and 100 deletions
@@ -4,8 +4,9 @@ import SwiftUI
@main @main
struct FuelBoardWidgetsBundle: WidgetBundle { struct FuelBoardWidgetsBundle: WidgetBundle {
var body: some Widget { var body: some Widget {
// Gallery order follows registration order: small first, then medium. // ONE widget kind (both sizes) first the iOS long-press app-icon
FuelPriceSmallWidget() // menu shows the FIRST-registered kind's sizes; this kind supports
// .systemSmall + .systemMedium so both tiles appear.
FuelPriceWidget() FuelPriceWidget()
FuelBoardLiveActivity() FuelBoardLiveActivity()
} }
+17 -50
View File
@@ -17,13 +17,14 @@ private extension FuelType {
} }
} }
// FuelBoard widget petrol stations near you. // FuelBoard widget petrol stations near you. ONE widget kind, TWO sizes:
// FuelPriceWidget (medium): top 3 stations with price + distance, each row // .systemSmall: single station (the first of the configured result the
// opens Maps. Per-widget fuel + sort (+ distance for Cheapest). // cheapest/closest station, or the cheapest favourite of the chosen fuel
// FuelPriceSmallWidget (small): single station, whole widget opens Maps. // in Favourites mode); the whole face opens Maps.
// Same knobs as the medium the small face shows the first of the // .systemMedium: top 3 stations with price + distance, each row opens Maps.
// configured result (cheapest/closest station, or the cheapest favourite // Per-widget fuel + sort (+ distance for Cheapest) via one config intent.
// of the chosen fuel in Favourites mode). // The view branches on widgetFamily, not kind the small face renders the
// first station, the medium face the list.
// Taps deep-link to Apple Maps directions (maps://?daddr=). On the Home // Taps deep-link to Apple Maps directions (maps://?daddr=). On the Home
// Screen the system either opens Maps directly or delivers the URL to // Screen the system either opens Maps directly or delivers the URL to
// FuelBoard, whose onOpenURL forwards it (and also still handles legacy // FuelBoard, whose onOpenURL forwards it (and also still handles legacy
@@ -49,6 +50,13 @@ private extension FuelType {
// (no permission yet / timeout) it falls back to the app's cached location, // (no permission yet / timeout) it falls back to the app's cached location,
// then to price-only sorting. So the widget is location-driven the moment the // then to price-only sorting. So the widget is location-driven the moment the
// app has been opened once and permission granted. // app has been opened once and permission granted.
//
// History: a separate "small" kind existed (FuelPriceWidgetSmall) and was
// merged here on 2026-08-14. The small kind had gained .systemMedium so the
// iOS long-press app-icon menu (which shows only the FIRST-registered kind's
// sizes) would offer both tiles but that duplicated the medium gallery
// entry. Merging keeps both size tiles in the icon menu AND one gallery entry
// with both sizes.
struct FuelPriceWidget: Widget { struct FuelPriceWidget: Widget {
let kind = "FuelPriceWidget" let kind = "FuelPriceWidget"
@@ -70,49 +78,8 @@ struct FuelPriceWidget: Widget {
} }
.configurationDisplayName("FuelBoard Prices") .configurationDisplayName("FuelBoard Prices")
.description("Fuel prices near you. Configure fuel + sort per widget.") .description("Fuel prices near you. Configure fuel + sort per widget.")
.supportedFamilies([.systemMedium])
.disfavoredLocations([.carPlay], for: [.systemMedium])
}
}
// Small widget exactly ONE station (the first of the configured result)
// when rendered at systemSmall; the SAME kind also supports .systemMedium,
// where the shared view renders the station list (identical content to the
// medium widget the view branches on family, not kind).
//
// WHY medium is here (2026-08-14): the iOS 18+ long-press app-icon menu
// ("turn app icon into a widget") shows the supported SIZES of the
// FIRST-registered widget kind only it does NOT list widget kinds, and it
// is NOT keyed by configuration-intent type (a two-intent experiment,
// 6375af4, changed nothing on-device). So the bundle's first kind decides
// which size tiles the icon menu offers: small-only meant the menu showed
// only the small tile even though the app has a medium widget. Giving this
// first-registered kind BOTH .systemSmall and .systemMedium puts both size
// tiles in the icon menu (ChatGPT/Safari show multiple sizes the same way
// their first kind supports several families). The gallery still lists both
// widget kinds in bundle order (small, then medium); this kind's entry just
// gains a medium variant. Favourites sort on the small face shows the
// cheapest favourite of the chosen fuel.
struct FuelPriceSmallWidget: Widget {
let kind = "FuelPriceWidgetSmall"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: FuelBoardSmallWidgetConfigurationIntent.self,
provider: FuelPriceTimelineProvider<FuelBoardSmallWidgetConfigurationIntent>(
beaconTag: "FuelBoardSmallWidget"
)
) { entry in
FuelPriceWidgetView(entry: entry)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
}
.configurationDisplayName("FuelBoard Small")
.description("Fuel prices near you. Configure fuel + sort per widget.")
.supportedFamilies([.systemSmall, .systemMedium]) .supportedFamilies([.systemSmall, .systemMedium])
.disfavoredLocations([.carPlay], for: [.systemSmall]) .disfavoredLocations([.carPlay], for: [.systemSmall, .systemMedium])
} }
} }
@@ -250,7 +217,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
// the cheapest favourite of the chosen fuel. (The old per-widget // the cheapest favourite of the chosen fuel. (The old per-widget
// pinned-favourite picker was removed: its AppEntity params made // pinned-favourite picker was removed: its AppEntity params made
// fresh-widget default-config resolution fail at the system level // fresh-widget default-config resolution fail at the system level
// see FuelPriceSmallWidget.) // the merged widget uses the minimal fuel/sort/distance intent.)
let fuelFavourites = favourites let fuelFavourites = favourites
.filter { $0.fuel == fuel && $0.station.prices[fuel] != nil } .filter { $0.fuel == fuel && $0.station.prices[fuel] != nil }
.map(\.station) .map(\.station)
-48
View File
@@ -120,51 +120,3 @@ struct FuelBoardWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConf
} }
} }
} }
// The SMALL widget's OWN intent TYPE. iOS keys the long-press app-icon
// contextual menu (iOS 18+ "turn icon into a widget") by configuration-intent
// TYPE, so two widget kinds sharing one intent collapse there to a single
// entry the first-registered kind. (The + gallery is kind-keyed, which is
// why it still lists both.) This intent carries the IDENTICAL minimal
// parameter set as the medium intent fuel/sort/distance only, NO AppEntity
// params (the favouriteFuel/favourite AppEntity params were what broke
// fresh-widget default-config resolution, not intent separation; see
// Fresh-widget defaults). Sharing the generic provider keeps the timeline
// logic in one place.
struct FuelBoardSmallWidgetConfigurationIntent: WidgetConfigurationIntent, WidgetConfigValues {
static var title: LocalizedStringResource = "Fuel & Sort"
static var description = IntentDescription("Which fuel, ordering and search radius this widget shows.")
@Parameter(title: "Fuel", default: .e10)
var fuel: WidgetFuel
@Parameter(title: "Sort by", default: .cheapest)
var sort: WidgetSort
@Parameter(title: "Distance", default: WidgetDistance(id: 5))
var distance: WidgetDistance
/// Edit-Widget UI: same layout as the medium intent Distance only for
/// Cheapest ordering; hidden for Closest and Favourites.
static var parameterSummary: some ParameterSummary {
When(\.$sort, .equalTo, WidgetSort.cheapest) {
Summary("Show \(\.$fuel) by \(\.$sort) within \(\.$distance)") {
\.$fuel
\.$sort
\.$distance
}
} otherwise: {
When(\.$sort, .equalTo, WidgetSort.favourites) {
Summary("Show \(\.$fuel) favourites") {
\.$fuel
\.$sort
}
} otherwise: {
Summary("Show \(\.$fuel) by \(\.$sort)") {
\.$fuel
\.$sort
}
}
}
}
}