// FuelBoardLiveActivityView.swift — renders the FuelBoard Live Activity. // // Lives in the widget extension (the standard host for ActivityConfiguration). // Shows the cheapest station for the pinned fuel within the app's chosen // radius. Tapping anywhere opens Apple Maps directions to that station. // // ADAPTIVE LAYOUT: ActivityConfiguration shares ONE content view across the // Lock Screen, banner, and the CarPlay small slot — there is no per-platform // closure. So this view provides two layouts and lets ViewThatFits pick by // available width: // • richBody — the full three-column design (glyph · fuel+station · price), // wins wherever there's Lock Screen width (it carries an // explicit minWidth so it can never be squeezed into the car). // • compactBody — a minimal price-strip (glyph+fuel+price, station caption // below) that wins in the CarPlay/Apple Watch Smart Stack // small slot. import ActivityKit import SwiftUI import WidgetKit /// The Live Activity itself — registered in the widget bundle alongside the /// regular price widget. No CarPlay entitlement involved: this renders on the /// Lock Screen, Dynamic Island, and the car display (CarPlay Ultra, iOS 26+). /// `.supplementalActivityFamilies([.small])` makes it eligible for the car's /// small Live Activity slot — display-only there (FuelBoard is not a /// CarPlay-enabled app, so car-side taps can't launch anything). struct FuelBoardLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: FuelBoardLiveActivityAttributes.self) { context in // Lock Screen / banner / CarPlay small slot presentation FuelBoardLiveActivityView(context: context) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { FuelBoardLiveActivityCompactView(context: context) } DynamicIslandExpandedRegion(.trailing) { FuelBoardLiveActivityPriceView(context: context) } DynamicIslandExpandedRegion(.bottom) { FuelBoardLiveActivityStationView(context: context) } } compactLeading: { Image(systemName: "fuelpump.fill") .foregroundStyle(.green) } compactTrailing: { FuelBoardLiveActivityPriceView(context: context) } minimal: { Text(context.state.priceText) .font(.caption2.bold().monospacedDigit()) } } .supplementalActivityFamilies([.small]) } } /// Lock Screen / banner / CarPlay body — adaptive. private struct FuelBoardLiveActivityView: View { let context: ActivityViewContext var body: some View { Link(destination: context.state.mapsURL) { ViewThatFits(in: .horizontal) { // rich first — wins on full-width Lock Screen / banner richBody // CarPlay / Watch small slot is far narrower than this, // so ViewThatFits reliably falls through to compactBody. .frame(minWidth: 280) // compact fallback — the car's small supplemental slot compactBody } } } /// Full three-column design (unchanged): brand glyph · fuel+station · price. private var richBody: some View { HStack(spacing: 12) { // LEFT — station brand glyph Image(systemName: "fuelpump.circle.fill") .font(.system(size: 32)) .foregroundStyle(.green, .white) .frame(width: 40, height: 40) // MIDDLE — fuel + station VStack(alignment: .leading, spacing: 2) { Text("Cheapest \(context.state.fuel.displayName)") .font(.headline) Text("\(context.state.stationName) · \(context.state.distanceText)") .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) // RIGHT — price VStack(alignment: .trailing, spacing: 2) { FuelStore.priceTextAttributed(context.state.pricePence, style: context.state.priceDisplayStyle, size: 22, weight: .bold) Text("Tap for directions") .font(.caption2) .foregroundStyle(.secondary) } } .padding() } /// Minimal strip for the small CarPlay / Watch Smart Stack slot: /// glyph + fuel left, bold price right, truncated station below. private var compactBody: some View { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 5) { Image(systemName: "fuelpump.fill") .font(.caption2) .foregroundStyle(.green) Text(context.state.fuel.displayName) .font(.caption2.weight(.semibold)) .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) } .padding(8) } } /// Dynamic Island expanded regions + compact trailing — price only. private struct FuelBoardLiveActivityPriceView: View { let context: ActivityViewContext var body: some View { FuelStore.priceTextAttributed(context.state.pricePence, style: context.state.priceDisplayStyle, size: 17, weight: .bold) } } /// Dynamic Island expanded — fuel + station name. private struct FuelBoardLiveActivityCompactView: View { let context: ActivityViewContext var body: some View { VStack(alignment: .leading, spacing: 2) { Text("Cheapest \(context.state.fuel.displayName)") .font(.caption2) .foregroundStyle(.secondary) Text(context.state.stationName) .font(.caption.bold()) .lineLimit(1) } } } /// Dynamic Island expanded bottom — station + distance. private struct FuelBoardLiveActivityStationView: View { let context: ActivityViewContext var body: some View { Text("\(context.state.stationName) · \(context.state.distanceText)") .font(.caption) .lineLimit(1) } } // MARK: - ContentState display helpers extension FuelBoardLiveActivityAttributes.ContentState { /// Display distance using the user's chosen unit (miles default). var distanceText: String { FuelStore.loadDistanceUnit().format(distanceKM) } /// Price string from pence, in the user's chosen display style /// (station sign "129.9" or pounds "£1.29⁹/L"). Uses the style captured /// in the state at push time. var priceText: String { FuelStore.priceText(pricePence, style: priceDisplayStyle) } /// Apple Maps directions URL to the pinned station. var mapsURL: URL { URL(string: "maps://?daddr=\(lat),\(lng)&t=d")! } }