Widget preview screen + shared widget rendering; dark-mode screenshots with real data

- Move widget entry view into Shared/FuelPriceWidgetViews.swift (FuelPriceWidgetContent,
  FuelPriceWidgetEntryData) so the app can render the REAL widget faces; widget
  extension now wraps it (FuelPriceEntry.data) — one source of truth for the face
- WidgetMockScreen (-widgets launch arg): app icon + small + medium widget previews
  for App Store captures; AppIconPreview image set (1024 drip master)
- AppStoreScreenshots: 5-8 dark-mode tab captures with real Halifax data, 9 widgets
- 95/95 tests green; Release IPA rebuilt + re-served
This commit is contained in:
FuelBoard Contributor
2026-08-15 16:25:10 +01:00
parent 24b6ca9bb2
commit 57121d5213
14 changed files with 316 additions and 160 deletions
+80
View File
@@ -0,0 +1,80 @@
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 {
var body: some View {
ZStack {
LinearGradient(
colors: [
Color(red: 0.09, green: 0.10, blue: 0.14),
Color(red: 0.02, green: 0.03, blue: 0.06)
],
startPoint: .top, endPoint: .bottom
)
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 26) {
// App icon + name, home-screen style
HStack(spacing: 16) {
Image("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)
Text("Cheapest fuel near you")
.font(.subheadline)
.foregroundStyle(.white.opacity(0.65))
}
}
.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 {
VStack(alignment: .leading, spacing: 10) {
Text(label.uppercased())
.font(.caption.weight(.semibold))
.tracking(1)
.foregroundStyle(.white.opacity(0.5))
FuelPriceWidgetContent(entry: mockEntry(family: family), family: family, isWidget: false)
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 24, style: .continuous))
}
}
/// Real stations (loaded dump), cheapest-first for the chosen fuel the
/// same selection the widget's own provider makes.
private func mockEntry(family: WidgetFamily) -> FuelPriceWidgetEntryData {
let fuel = FuelType.e10
let all = FuelStore.loadStations()
let selling = all.filter { $0.prices[fuel] != nil }
.sorted { ($0.prices[fuel] ?? .infinity) < ($1.prices[fuel] ?? .infinity) }
let location = FuelStore.loadLocation().map { Coordinate(lat: $0.lat, lng: $0.lng) }
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()
)
}
}