Unify disabled states and preserve station verdicts
This commit is contained in:
@@ -533,7 +533,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -599,7 +599,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -663,7 +663,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -728,7 +728,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -826,7 +826,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
@@ -851,7 +851,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Config/App.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
@@ -875,7 +875,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
@@ -900,7 +900,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/Widget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
|
||||
+29
-24
@@ -166,9 +166,8 @@ struct AlertsView: View {
|
||||
}
|
||||
|
||||
/// An option row that belongs to a toggle. When the toggle is off the control
|
||||
/// is replaced by a dimmed, static value row: `.disabled` is not enough here
|
||||
/// because an inline-style picker in a List still expands its options when
|
||||
/// tapped while disabled.
|
||||
/// is replaced by a static value row. The enclosing dependent group applies
|
||||
/// FuelBoard's shared disabled interaction and appearance once.
|
||||
@ViewBuilder
|
||||
private func optionRow<Content: View>(_ title: String,
|
||||
value: String,
|
||||
@@ -182,7 +181,6 @@ struct AlertsView: View {
|
||||
Spacer()
|
||||
Text(value)
|
||||
}
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,28 +189,30 @@ struct AlertsView: View {
|
||||
List {
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: favouriteDropFuel.displayName,
|
||||
isEnabled: favouriteDropEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
Group {
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: favouriteDropFuel.displayName,
|
||||
isEnabled: favouriteDropEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
|
||||
ForEach(FuelType.allCases) { fuel in
|
||||
Text(fuel.displayName).tag(fuel)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
||||
Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
|
||||
.disabled(!favouriteDropEnabled)
|
||||
.allowsHitTesting(favouriteDropEnabled)
|
||||
Text(String(localized: "Get notified when a different favourite becomes the cheapest, or your cheapest favourite drops in price.", comment: "Favourite price alerts section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fuelBoardDisabledState(isEnabled: favouriteDropEnabled)
|
||||
} header: {
|
||||
Text(String(localized: "Favourite price alerts", comment: "Favourite price alerts section title"))
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
|
||||
optionRow(String(localized: "Alert type", comment: "Proximity alert type picker title"),
|
||||
Group {
|
||||
optionRow(String(localized: "Alert type", comment: "Proximity alert type picker title"),
|
||||
value: alertMode.displayName,
|
||||
isEnabled: enabled) {
|
||||
Picker(String(localized: "Alert type", comment: "Proximity alert type picker title"), selection: $alertMode) {
|
||||
@@ -265,9 +265,11 @@ struct AlertsView: View {
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(String(localized: "Background alerts depend on when your iPhone allows them; each station alerts at most once an hour.", comment: "Proximity alerts background limitation note"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.fuelBoardDisabledState(isEnabled: enabled)
|
||||
} header: {
|
||||
Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
|
||||
}
|
||||
@@ -281,7 +283,8 @@ struct AlertsView: View {
|
||||
|
||||
Section {
|
||||
Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
Group {
|
||||
optionRow(String(localized: "Fuel", comment: "Fuel picker title"),
|
||||
value: liveActivityFuel.displayName,
|
||||
isEnabled: liveActivityEnabled) {
|
||||
Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
|
||||
@@ -305,9 +308,11 @@ struct AlertsView: View {
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(String(localized: "See the cheapest nearby station on your Lock Screen and Dynamic Island, with updates as you travel.", comment: "Live Activity section description"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.fuelBoardDisabledState(isEnabled: liveActivityEnabled)
|
||||
} header: {
|
||||
Text(String(localized: "Live Activity", comment: "Live Activity section title"))
|
||||
}
|
||||
|
||||
+17
-31
@@ -1842,26 +1842,13 @@ struct StationRow: View {
|
||||
/// 22pt keeps the price the row's loudest element after the station name.
|
||||
@ScaledMetric(relativeTo: .title2) private var priceSize: CGFloat = 22
|
||||
|
||||
/// The identity column's share of the ROW, for every row and both price
|
||||
/// styles. The mockup's divider sits at ~53% of its row; this is that ratio,
|
||||
/// held per row rather than derived from the row's text — the point is that
|
||||
/// the hairline, the price axis and the star cannot move as the list scrolls.
|
||||
/// The price column takes the remainder, and its savings capsule scales down
|
||||
/// instead of widening the column and dragging the hairline with it.
|
||||
///
|
||||
/// One value for both price styles: the star used to sit at the end of the
|
||||
/// price line and hold ~28pt of the price column, so the wide form `£1.58⁷/L`
|
||||
/// — ~20pt wider than the bare `158.7` at the same point size — did not fit on
|
||||
/// a 393pt screen. With the star now under the station logo the price column
|
||||
/// has that width back, so the identity column no longer has to give width up
|
||||
/// in pounds & pence, and names keep the same room in both styles.
|
||||
private static let leftSectionShare: CGFloat = 0.52
|
||||
|
||||
/// The comparison label sits beside the capsule only where the row is wide
|
||||
/// enough for both (regular width). On an iPhone the mockup's canvas is
|
||||
/// wider than the whole screen, so the label drops under the capsule rather
|
||||
/// than starving the station name.
|
||||
@Environment(\.horizontalSizeClass) private var sizeClass
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
|
||||
private var ragColor: Color {
|
||||
guard let price = station.prices[fuel], let baselinePrice else { return .gray }
|
||||
@@ -1931,13 +1918,9 @@ struct StationRow: View {
|
||||
// the detail sheet instead of being repeated on every row.
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
// LEFT SECTION — station identity: brand logo, then name, address and
|
||||
// distance. Given a FIXED SHARE of the row (~52%, the mockup's ratio)
|
||||
// rather than sizing to its content: sized to content, this column
|
||||
// grew and shrank with each row's badge and savings copy, which moved
|
||||
// the hairline 23pt between adjacent rows (x767 on a BEST VALUE row,
|
||||
// x699 on a "costs more" row) — so the divider and the price axis
|
||||
// both jumped sideways as the list scrolled. One share for every row
|
||||
// puts them on a single vertical line down the whole list.
|
||||
// distance. This section yields horizontal space to the financial
|
||||
// column first: a truncated station identity is recoverable from the
|
||||
// detail sheet, while a truncated saving is materially misleading.
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
// The logo, with the favourite star directly beneath it. Moving
|
||||
// the star out of the price line is what gives the price column
|
||||
@@ -1972,16 +1955,15 @@ struct StationRow: View {
|
||||
|
||||
// Name, full address, distance
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
// The name WRAPS instead of truncating: the row is tall
|
||||
// enough now, and a name cut to "Z A Service Stat…" cannot
|
||||
// be told apart from the station next door. Baseline
|
||||
// alignment keeps the notice symbols on the name's FIRST
|
||||
// line instead of floating beside a two-line name.
|
||||
// Keep the established compact row. Long names yield space
|
||||
// to the complete price/verdict column rather than wrapping
|
||||
// and forcing a financial amount to truncate.
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
Text(station.name)
|
||||
.font(.headline)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
ForEach(StationNotice.notices(for: station)) { notice in
|
||||
Image(systemName: notice.symbolName)
|
||||
.font(.caption2.weight(.semibold))
|
||||
@@ -2019,11 +2001,10 @@ struct StationRow: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.layoutPriority(0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.containerRelativeFrame(.horizontal, alignment: .leading) { length, _ in
|
||||
length * Self.leftSectionShare
|
||||
}
|
||||
.layoutPriority(0)
|
||||
|
||||
// 1pt hairline between the two sections. Inset top and bottom so it
|
||||
// spans the row's content without touching the row edges, and
|
||||
@@ -2075,7 +2056,12 @@ struct StationRow: View {
|
||||
savingsCapsule(result)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
// The verdict owns its natural one-line width at normal Dynamic Type
|
||||
// sizes. The identity column compresses first, preventing "Approx.
|
||||
// Save £…" while keeping the same two-column visual design.
|
||||
.fixedSize(horizontal: !dynamicTypeSize.isAccessibilitySize,
|
||||
vertical: false)
|
||||
.layoutPriority(1)
|
||||
// The spec's ~20-24pt of air between the divider and the price
|
||||
// column: the row's own 12pt spacing plus this 10pt.
|
||||
.padding(.leading, 10)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Applies FuelBoard's shared unavailable appearance and native interaction state
|
||||
/// to a complete dependent row or group. Apply it once at the highest dependent
|
||||
/// container so labels, values, icons, descriptions and controls fade together.
|
||||
extension View {
|
||||
func fuelBoardDisabledState(isEnabled: Bool) -> some View {
|
||||
disabled(!isEnabled)
|
||||
.allowsHitTesting(isEnabled)
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
}
|
||||
}
|
||||
@@ -105,25 +105,26 @@ struct FuelSavingsCapsule: View {
|
||||
let result: FuelSavingsResult
|
||||
var compact: Bool = true
|
||||
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: compact ? 3 : 5) {
|
||||
Image(systemName: Self.symbol(for: result.status))
|
||||
.accessibilityHidden(true)
|
||||
Text(result.capsuleHeadline())
|
||||
.lineLimit(1)
|
||||
// Measured on a 393pt screen: the capsule is handed ~101pt, of
|
||||
// which the icon, its spacing and the 8pt padding either side
|
||||
// take 32pt, leaving ~69pt for the text. "Closest station" needs
|
||||
// ~90pt at full size, so a 0.8 floor — 72pt — could not reach it
|
||||
// and the row showed "Closest stati…" instead. 0.7 leaves ~63pt,
|
||||
// which clears it at ~10pt type: scaled, never truncated.
|
||||
.minimumScaleFactor(0.7)
|
||||
// Financial verdicts stay complete and single-line at ordinary
|
||||
// text sizes. Accessibility sizes may use a second line rather
|
||||
// than clipping, shrinking or losing the amount to an ellipsis.
|
||||
.lineLimit(dynamicTypeSize.isAccessibilitySize ? 2 : 1)
|
||||
.fixedSize(horizontal: !dynamicTypeSize.isAccessibilitySize,
|
||||
vertical: true)
|
||||
}
|
||||
.font(compact ? .caption.weight(.semibold) : .headline)
|
||||
.foregroundStyle(Self.color(for: result.status))
|
||||
.padding(.horizontal, compact ? 8 : 12)
|
||||
.padding(.vertical, compact ? 4 : 7)
|
||||
.background(Capsule().fill(Self.color(for: result.status).opacity(0.15)))
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
/// Verdict colour: green save, red worse off, grey break even, blue
|
||||
|
||||
@@ -198,7 +198,7 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
.settingsDependencyEnabled(fuelSavingsSettings.enabled)
|
||||
.fuelBoardDisabledState(isEnabled: fuelSavingsSettings.enabled)
|
||||
} header: {
|
||||
Text("Fuel Savings")
|
||||
} footer: {
|
||||
@@ -582,7 +582,7 @@ struct SettingsView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.settingsDependencyEnabled(!tipStore.purchaseInProgress)
|
||||
.fuelBoardDisabledState(isEnabled: !tipStore.purchaseInProgress)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -925,16 +925,6 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the native unavailable state to a complete dependent settings group
|
||||
/// and also dims non-control content (read-outs, icons and explanatory text),
|
||||
/// which does not automatically adopt a disabled appearance from SwiftUI.
|
||||
private extension View {
|
||||
func settingsDependencyEnabled(_ isEnabled: Bool) -> some View {
|
||||
disabled(!isEnabled)
|
||||
.opacity(isEnabled ? 1 : 0.65)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the three tip tiers and drives their purchases.
|
||||
@MainActor
|
||||
final class TipStore: ObservableObject {
|
||||
|
||||
Reference in New Issue
Block a user