live activity: compact CarPlay strip shows fuel + price + station distance

Restore .supplementalActivityFamilies([.small]) + a ViewThatFits compact
fallback so CarPlay's small slot renders our shared compact strip (fuel type,
bold price, station · distance) instead of falling back to the Dynamic Island
compact closure, which showed the app name and omitted the distance the user
wants. Full iPhone/iPad card is preserved via the richMinWidth gate on
richBody plus its flexible, truncating middle column (long names can no longer
collapse it).
This commit is contained in:
FuelBoard Contributor
2026-08-19 18:31:36 +01:00
parent 5d26509034
commit 01bb3ba905
@@ -49,12 +49,15 @@ struct FuelBoardLiveActivity: Widget {
.font(.caption2.bold().monospacedDigit())
}
}
// NOTE: deliberately NO `.supplementalActivityFamilies([.small])`.
// That modifier makes iOS eligible to render this activity in the
// narrow `.small` form on the iPhone/iPad Lock Screen, which is what
// produced the squeezed, small-text card. Dropping it keeps the
// full-width Lock Screen card on iPhone/iPad; CarPlay still shows a
// small form via the Dynamic Island compact closures below.
.supplementalActivityFamilies([.small])
// Why `.small` is kept: it lets the SHARED body render a compact form
// in genuinely small slots (CarPlay small / Apple Watch smart stack)
// instead of falling back to the Dynamic Island compact closure
// which could NOT show the station distance the user wants on CarPlay.
// The full-width iPhone/iPad card is protected by the `richMinWidth`
// gate on `richBody` + its flexible, truncating middle column, so
// iPhone/iPad still get the full card; only truly small space picks
// the compact strip below.
}
}
@@ -62,19 +65,28 @@ struct FuelBoardLiveActivity: Widget {
private struct FuelBoardLiveActivityView: View {
let context: ActivityViewContext<FuelBoardLiveActivityAttributes>
/// Full card needs at least this much width; iPhone/iPad Lock Screen is
/// always wider, so they always get it. Only genuinely small slots
/// (CarPlay small / Watch smart stack) fall through to `compactBody`.
private let richMinWidth: CGFloat = 280
/// Cap the compact strip so a mid-width surface can't sneak it in.
private let compactMaxWidth: CGFloat = 230
var body: some View {
Link(destination: context.state.mapsURL) {
// Always the full three-column card. The station caption is
// line-limited + scale-down + tail-truncated, so a LONG station
// name truncates in place instead of inflating this view's ideal
// width and tricking ViewThatFits into falling back to the compact
// strip (that is exactly what made 5-mi / long-named activities
// render small while 10-15-mi / short names stayed full).
//
// No ViewThatFits / compactBody: with `.supplementalActivityFamilies`
// removed, this body is only ever handed Lock-Screen width, so the
// compact fallback was both dead weight and the cause of the bug.
ViewThatFits(in: .horizontal) {
// rich FIRST wins on the full-width iPhone/iPad Lock Screen.
// Its middle column is flexible + line-limited, so a LONG
// station name truncates in place and never inflates the ideal
// width (which used to trick ViewThatFits into falling back to
// compact on iPhone).
richBody
.frame(minWidth: richMinWidth)
// compact fallback the small CarPlay / Watch smart-stack
// strip: fuel · price · station distance (no app name).
compactBody
.frame(maxWidth: compactMaxWidth)
}
}
}
@@ -113,7 +125,31 @@ private struct FuelBoardLiveActivityView: View {
.padding()
}
// NOTE: `compactBody` was removed always render `richBody` (see body).
/// Minimal strip for small space (CarPlay small / Watch smart stack):
/// fuel type + bold price on one line, station · distance below.
/// Deliberately no app name and no "Tap for directions" CarPlay is
/// display-only, and the user's asks here are just fuel + price + distance.
private var compactBody: some View {
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 5) {
Text(context.state.fuel.displayName)
.font(.caption.bold())
.lineLimit(1)
Spacer(minLength: 4)
FuelStore.priceTextAttributed(context.state.pricePence,
style: context.state.priceDisplayStyle,
size: 15, weight: .bold)
.lineLimit(1)
}
Text("\\(context.state.stationName) · \\(context.state.distanceText)")
.font(.system(size: 9))
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
.truncationMode(.tail)
}
.padding(8)
}
}
/// Dynamic Island expanded regions + compact trailing price only.