import SwiftUI import WidgetKit /// Full-screen widget preview (launch arg `-widgets`): the real app icon /// plus the REAL widget faces (Shared/FuelPriceWidgetViews.swift — the same /// code the widget extension renders) at small + medium sizes. Used for App /// Store screenshot captures; not reachable from the UI. struct WidgetMockScreen: View { @Environment(\.colorScheme) private var colorScheme var body: some View { ZStack { // Real iPhone wallpaper (extracted from the iOS 26.3 sim runtime — // ActionButtonSelector Display.ca asset, 1320×2870 native res). Image("PreviewWallpaper") .resizable() .scaledToFill() .ignoresSafeArea() VStack(alignment: .leading, spacing: 26) { // App icon + name, home-screen style. Uses the SAME artwork // the app ships (AppIcon.appiconset), with the dark variant // when the device is in dark mode — matching iOS behaviour. HStack(spacing: 16) { Image(colorScheme == .dark ? "AppIconPreviewDark" : "AppIconPreview") .resizable() .frame(width: 76, height: 76) .clipShape(RoundedRectangle(cornerRadius: 17, style: .continuous)) VStack(alignment: .leading, spacing: 3) { Text("FuelBoard") .font(.title2.bold()) .foregroundStyle(.white) .shadow(radius: 2, y: 1) Text("Cheapest fuel near you") .font(.subheadline) .foregroundStyle(.white.opacity(0.9)) .shadow(radius: 2, y: 1) } } .padding(.top, 8) widgetCard(.systemSmall, label: "Small widget") widgetCard(.systemMedium, label: "Medium widget") Spacer(minLength: 0) } .padding(30) } } private func widgetCard(_ family: WidgetFamily, label: String) -> some View { // Real widget proportions: system small content is 158×158pt, // medium 338×158pt (card adds the 14pt padding each side). let contentW: CGFloat = family == .systemSmall ? 158 : 338 let cardW: CGFloat = contentW + 28 let cardH: CGFloat = contentW + 28 return VStack(alignment: .leading, spacing: 10) { Text(label.uppercased()) .font(.caption.weight(.semibold)) .tracking(1) .foregroundStyle(.secondary) FuelPriceWidgetContent(entry: mockEntry(family: family), family: family, isWidget: false) .padding(14) .frame(width: cardW, height: cardH, alignment: .leading) .background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 24, style: .continuous)) } } /// Real stations (loaded dump) for the widget's DEFAULT configuration — /// Unleaded / Cheapest / 5 miles — with the SAME radius test the widget's /// provider applies. Without it this preview happily listed the UK-wide /// cheapest (Banbridge, 184 mi away) under "Cheapest fuel near you", which /// is not what the widget ever shows. private func mockEntry(family: WidgetFamily) -> FuelPriceWidgetEntryData { let fuel = FuelType.e10 let location = FuelStore.loadLocation().map { Coordinate(lat: $0.lat, lng: $0.lng) } // 5 miles — the widget config's default Distance. WidgetDistance itself // lives in the widget target, so the preview mirrors its default here. let radiusKM = 5 * 1.60934 let all = FuelStore.loadStations() let selling = all.filter { station in guard station.prices[fuel] != nil else { return false } guard let location else { return true } return FuelStore.isWithinDisplayRadius(station: station, radiusKM: radiusKM, userLat: location.lat, userLng: location.lng) } .sorted { ($0.prices[fuel] ?? .infinity) < ($1.prices[fuel] ?? .infinity) } let count = family == .systemSmall ? 1 : 3 return FuelPriceWidgetEntryData( date: Date(), stations: Array(selling.prefix(count)), fuel: fuel, sort: .cheapest, isFavourites: false, location: location, locationSource: location != nil ? "live" : "none", unit: FuelStore.loadDistanceUnit() ) } }