Widget: keychain-free fresh defaults — drop defaultResult(), auto-populate at entry time
The small-widget skeleton (fresh widget stranded on the placeholder while the medium populated in the gallery) traced to the small intent's default configuration resolution: FavouriteFuelQuery/WidgetFavouriteQuery defaultResult() read keychain in the gallery-preview/fresh-add path, failing the whole default config and leaving no timeline. Configured widgets bypassed defaultResult (stored values → lenient entities) — the exact favourites-works/cheapest-closest-doesn't split. - Remove defaultResult() from both queries: fresh configs now resolve from the static @Parameter(default:) values through lenient entities(for:) with ZERO storage reads. - WidgetFavouriteQuery.entities(for:) skips the keychain name lookup for id-only (empty station) values. - Auto-populate moves to makeEntry: empty pinned favourite → first favourite of the fuel (same rendered result as the old sheet default). - Sheet keeps the dependent picker via suggestedEntities() + dependency; fresh widgets show static defaults until picked (approved tradeoff).
This commit is contained in:
@@ -165,16 +165,29 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
|
|||||||
// fuel, no radius. Freshen its price when a focused fetch covers
|
// fuel, no radius. Freshen its price when a focused fetch covers
|
||||||
// it. The chosen favourite wins; when it's missing or stale (fresh
|
// it. The chosen favourite wins; when it's missing or stale (fresh
|
||||||
// widget, favourite removed, fuel row changed) fall back to the
|
// widget, favourite removed, fuel row changed) fall back to the
|
||||||
// favourites fuel's cheapest favourite so the face is never blank.
|
// favourites fuel's first favourite so the face is never blank.
|
||||||
if let chosen = configuration.favouriteChoice {
|
if let chosen = configuration.favouriteChoice {
|
||||||
let favouriteFuelType = configuration.favouriteFuelChoice?.fuel ?? fuel
|
let favouriteFuelType = configuration.favouriteFuelChoice?.fuel ?? fuel
|
||||||
let pinned = favourites.first { $0.id == chosen.id }
|
// Fresh widget / static default (empty pinned favourite):
|
||||||
|
// auto-populate to the FIRST favourite of the fuel. This is
|
||||||
|
// the sheet's old defaultResult() auto-populate, moved to
|
||||||
|
// entry time so default-config resolution stays keychain-free
|
||||||
|
// (storage-backed defaultResult() stranding fresh small
|
||||||
|
// widgets on the skeleton placeholder).
|
||||||
|
let pinned: FavouriteEntry?
|
||||||
|
if chosen.stationID.isEmpty {
|
||||||
|
pinned = favourites
|
||||||
|
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
|
||||||
|
.first
|
||||||
|
} else {
|
||||||
|
pinned = favourites.first { $0.id == chosen.id }
|
||||||
?? favourites
|
?? favourites
|
||||||
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
|
.filter { $0.fuel == favouriteFuelType && $0.station.prices[favouriteFuelType] != nil }
|
||||||
.min { lhs, rhs in
|
.min { lhs, rhs in
|
||||||
(lhs.station.prices[favouriteFuelType] ?? .infinity) <
|
(lhs.station.prices[favouriteFuelType] ?? .infinity) <
|
||||||
(rhs.station.prices[favouriteFuelType] ?? .infinity)
|
(rhs.station.prices[favouriteFuelType] ?? .infinity)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let pinned {
|
if let pinned {
|
||||||
var station = pinned.station
|
var station = pinned.station
|
||||||
if let location,
|
if let location,
|
||||||
|
|||||||
@@ -95,9 +95,15 @@ struct FavouriteFuelQuery: EntityQuery {
|
|||||||
// LENIENT resolution: every valid fuel resolves, even one without
|
// LENIENT resolution: every valid fuel resolves, even one without
|
||||||
// favourites. The timeline request resolves the FULL configuration
|
// favourites. The timeline request resolves the FULL configuration
|
||||||
// through these queries, and a nil result fails the whole timeline — a
|
// through these queries, and a nil result fails the whole timeline — a
|
||||||
// default-config widget would hang on the skeleton placeholder. Fresh
|
// default-config widget would hang on the skeleton placeholder.
|
||||||
// widgets still auto-populate via defaultResult(); the picker options
|
//
|
||||||
// stay filtered in suggestedEntities().
|
// NO defaultResult(): fresh/default configs resolve from the static
|
||||||
|
// @Parameter(default:) value through entities(for:) with ZERO storage
|
||||||
|
// reads. The old keychain-backed defaultResult() put storage access in
|
||||||
|
// the gallery-preview/fresh-add resolution path, which failed on-device
|
||||||
|
// (small widget stranded on the skeleton; configured widgets were fine).
|
||||||
|
// Auto-populate for the SHEET moved to entry time (makeEntry picks the
|
||||||
|
// first favourite of the fuel); picker options stay in suggestedEntities().
|
||||||
func entities(for identifiers: [String]) async throws -> [FavouriteFuel] {
|
func entities(for identifiers: [String]) async throws -> [FavouriteFuel] {
|
||||||
identifiers.compactMap { FuelType(rawValue: $0) }.map { FavouriteFuel(fuel: $0) }
|
identifiers.compactMap { FuelType(rawValue: $0) }.map { FavouriteFuel(fuel: $0) }
|
||||||
}
|
}
|
||||||
@@ -106,15 +112,6 @@ struct FavouriteFuelQuery: EntityQuery {
|
|||||||
let fuels = Set(FuelStore.loadFavourites().map(\.fuel))
|
let fuels = Set(FuelStore.loadFavourites().map(\.fuel))
|
||||||
return FuelType.allCases.filter { fuels.contains($0) }.map { FavouriteFuel(fuel: $0) }
|
return FuelType.allCases.filter { fuels.contains($0) }.map { FavouriteFuel(fuel: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fresh widget / unresolved value → auto-populate the Fuel row with the
|
|
||||||
/// FIRST fuel that has a favourite (Unleaded, Premium, Diesel order).
|
|
||||||
func defaultResult() async -> FavouriteFuel? {
|
|
||||||
let fuels = Set(FuelStore.loadFavourites().map(\.fuel))
|
|
||||||
return FuelType.allCases.first { fuels.contains($0) }
|
|
||||||
.map { FavouriteFuel(fuel: $0) }
|
|
||||||
?? FavouriteFuel(fuel: .e10)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A pinned favourite station, selectable on SMALL widgets (which show a single
|
// A pinned favourite station, selectable on SMALL widgets (which show a single
|
||||||
@@ -164,13 +161,27 @@ struct WidgetFavouriteQuery: EntityQuery {
|
|||||||
// WidgetFavourite (best-effort name lookup). NEVER return nil here — the
|
// WidgetFavourite (best-effort name lookup). NEVER return nil here — the
|
||||||
// timeline request resolves the whole configuration through this query
|
// timeline request resolves the whole configuration through this query
|
||||||
// and a failed resolution strands a default-config widget on the skeleton
|
// and a failed resolution strands a default-config widget on the skeleton
|
||||||
// placeholder. Fresh widgets auto-populate via defaultResult().
|
// placeholder.
|
||||||
|
//
|
||||||
|
// NO defaultResult(): fresh/default configs resolve from the static
|
||||||
|
// @Parameter(default:) value (empty id "e10|") with NO storage reads —
|
||||||
|
// the old keychain-backed defaultResult() ran in the gallery-preview /
|
||||||
|
// fresh-add resolution path and failed on-device. The sheet's
|
||||||
|
// auto-populate moved to entry time (makeEntry pins the first favourite
|
||||||
|
// of the fuel); the dependent picker keeps working via suggestedEntities().
|
||||||
func entities(for identifiers: [String]) async throws -> [WidgetFavourite] {
|
func entities(for identifiers: [String]) async throws -> [WidgetFavourite] {
|
||||||
let favourites = FuelStore.loadFavourites()
|
|
||||||
return identifiers.map { id in
|
return identifiers.map { id in
|
||||||
let parts = id.split(separator: "|", maxSplits: 1)
|
let parts = id.split(separator: "|", maxSplits: 1)
|
||||||
let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10
|
let fuel = FuelType(rawValue: String(parts.first ?? "")) ?? .e10
|
||||||
let stationID = String(parts.last ?? "")
|
let stationID = String(parts.last ?? "")
|
||||||
|
// Id-only value (fresh widget's static default): resolve with
|
||||||
|
// zero keychain access. displayRepresentation falls back to a
|
||||||
|
// storage lookup if the name is empty, so stored picks still
|
||||||
|
// render their name.
|
||||||
|
guard !stationID.isEmpty else {
|
||||||
|
return WidgetFavourite(fuel: fuel, stationID: stationID, stationName: "")
|
||||||
|
}
|
||||||
|
let favourites = FuelStore.loadFavourites()
|
||||||
let entry = favourites.first { $0.id == id }
|
let entry = favourites.first { $0.id == id }
|
||||||
return WidgetFavourite(
|
return WidgetFavourite(
|
||||||
fuel: fuel,
|
fuel: fuel,
|
||||||
@@ -196,26 +207,6 @@ struct WidgetFavouriteQuery: EntityQuery {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fresh widget / unresolved value → auto-populate the Favourite row with
|
|
||||||
/// the FIRST favourite of the Fuel row's fuel (falling back to the first
|
|
||||||
/// favourite of any fuel if the fuel has none, e.g. mid-change).
|
|
||||||
func defaultResult() async -> WidgetFavourite? {
|
|
||||||
let favourites = FuelStore.loadFavourites()
|
|
||||||
guard !favourites.isEmpty else { return nil }
|
|
||||||
let entry: FavouriteEntry
|
|
||||||
if let fuel = smallIntent?.favouriteFuel.fuel,
|
|
||||||
let scoped = favourites.first(where: { $0.fuel == fuel }) {
|
|
||||||
entry = scoped
|
|
||||||
} else {
|
|
||||||
entry = favourites[0]
|
|
||||||
}
|
|
||||||
return WidgetFavourite(
|
|
||||||
fuel: entry.fuel,
|
|
||||||
stationID: entry.station.id,
|
|
||||||
stationName: entry.station.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The knobs both the medium-list and small-single widget intents expose, so
|
// The knobs both the medium-list and small-single widget intents expose, so
|
||||||
|
|||||||
Reference in New Issue
Block a user