Simplify CarPlay Live Activity to letter+arrow glance indicator
Replaces the full sentence ("Heck steht höher, links steht höher") in
the .small activity family with a compact "H ↑ L" style readout: a
direction letter per axis (only shown when outside tolerance) around a
single up arrow, since only the higher side is ever named anyway. Text
now shares the same green/orange/red scale as the bubble glyph (factored
into one levelColour helper instead of two duplicated thresholds), and
uses a scalable font style with minimumScaleFactor so it fits whatever
size CarPlay actually grants the card - there's no API to query the
physical display size from Live Activity content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1b06ac36d1
commit
0fdef337fc
@@ -19,6 +19,20 @@ private struct LevelIcon: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Muss mit `LevelState.levelTolerance` übereinstimmen.
|
||||||
|
private let levelTolerance: Double = 0.5
|
||||||
|
|
||||||
|
/// Grün bis Toleranz, orange bis 2°, sonst rot – dieselbe Skala für Blase
|
||||||
|
/// und Text, wie in der App (`VehicleTilt.colour`). Hier dupliziert, weil
|
||||||
|
/// die Extension `Shared/` nicht mitkompiliert (siehe
|
||||||
|
/// `LevelActivityAttributes.swift`).
|
||||||
|
private func levelColour(forDeviation deviation: Double?) -> Color {
|
||||||
|
guard let deviation else { return .white.opacity(0.4) }
|
||||||
|
if deviation <= levelTolerance { return .green }
|
||||||
|
if deviation <= 2 { return .orange }
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
|
||||||
/// Verkleinerte Libelle für Sperrbildschirm/CarPlay und die erweiterte
|
/// Verkleinerte Libelle für Sperrbildschirm/CarPlay und die erweiterte
|
||||||
/// Dynamic Island – dieselbe Optik wie `LevelBubble` in der App, aber ohne
|
/// Dynamic Island – dieselbe Optik wie `LevelBubble` in der App, aber ohne
|
||||||
/// deren Abhängigkeit auf `Shared/Models/LevelState.swift`, das in der
|
/// deren Abhängigkeit auf `Shared/Models/LevelState.swift`, das in der
|
||||||
@@ -31,17 +45,12 @@ private struct LevelBubbleGlyph: View {
|
|||||||
|
|
||||||
/// Bis zu welcher Neigung die Blase ausschlägt, wie in der App-Libelle.
|
/// Bis zu welcher Neigung die Blase ausschlägt, wie in der App-Libelle.
|
||||||
private let range: Double = 6
|
private let range: Double = 6
|
||||||
/// Muss mit `LevelState.levelTolerance` übereinstimmen.
|
|
||||||
private let tolerance: Double = 0.5
|
|
||||||
|
|
||||||
private var hasReading: Bool { pitch != nil || roll != nil }
|
private var hasReading: Bool { pitch != nil || roll != nil }
|
||||||
|
|
||||||
private var bubbleColor: Color {
|
private var bubbleColor: Color {
|
||||||
guard let pitch, let roll else { return .white.opacity(0.4) }
|
guard let pitch, let roll else { return .white.opacity(0.4) }
|
||||||
let deviation = max(abs(pitch), abs(roll))
|
return levelColour(forDeviation: max(abs(pitch), abs(roll)))
|
||||||
if deviation <= tolerance { return .green }
|
|
||||||
if deviation <= 2 { return .orange }
|
|
||||||
return .red
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -50,7 +59,7 @@ private struct LevelBubbleGlyph: View {
|
|||||||
let travel = radius - bubble / 2 - 3
|
let travel = radius - bubble / 2 - 3
|
||||||
let offsetX = clamped(roll) / range * travel
|
let offsetX = clamped(roll) / range * travel
|
||||||
let offsetY = clamped(pitch) / range * travel
|
let offsetY = clamped(pitch) / range * travel
|
||||||
let toleranceRadius = max(tolerance / range * travel, bubble * 0.55)
|
let toleranceRadius = max(levelTolerance / range * travel, bubble * 0.55)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
@@ -137,28 +146,67 @@ private struct LevelActivityContent: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Kompaktform für CarPlay-Dashboard und Smart Stack der Uhr: Libelle plus
|
/// Kompaktform für CarPlay-Dashboard und Smart Stack der Uhr: Libelle plus
|
||||||
/// eine Textzeile, ohne die längere Zwei-Zeilen-Aufschlüsselung des
|
/// ein Blick-Symbol statt eines ganzen Satzes – zum Lesen im Vorbeifahren.
|
||||||
/// Sperrbildschirms.
|
///
|
||||||
|
/// Gezeigt wird nur, welche Seite höher steht: Buchstabe (Längsneigung),
|
||||||
|
/// Pfeil, Buchstabe (Querneigung) – z.B. "H ↑ L" für "Heck und links stehen
|
||||||
|
/// höher". Ein einzelner Pfeil in der Mitte reicht, weil ohnehin nur die
|
||||||
|
/// höher stehende Seite benannt wird ("höher" ist implizit "nach oben").
|
||||||
private struct CompactLevelView: View {
|
private struct CompactLevelView: View {
|
||||||
let attributes: LevelActivityAttributes
|
let attributes: LevelActivityAttributes
|
||||||
let state: LevelActivityAttributes.ContentState
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|
||||||
|
private var hasReading: Bool { state.pitch != nil || state.roll != nil }
|
||||||
|
|
||||||
|
/// "H"/"F", nur wenn ausserhalb der Toleranz.
|
||||||
|
private var pitchLetter: String? {
|
||||||
|
guard let pitch = state.pitch, abs(pitch) > levelTolerance else { return nil }
|
||||||
|
return pitch >= 0 ? "H" : "F"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "R"/"L", nur wenn ausserhalb der Toleranz.
|
||||||
|
private var rollLetter: String? {
|
||||||
|
guard let roll = state.roll, abs(roll) > levelTolerance else { return nil }
|
||||||
|
return roll >= 0 ? "R" : "L"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dieselbe Skala wie die Blase, über die grössere der beiden Neigungen.
|
||||||
|
private var statusColor: Color {
|
||||||
|
guard let pitch = state.pitch, let roll = state.roll else { return .white.opacity(0.4) }
|
||||||
|
return levelColour(forDeviation: max(abs(pitch), abs(roll)))
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 14) {
|
||||||
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel, diameter: 40)
|
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel, diameter: 40)
|
||||||
|
|
||||||
Text(state.instruction
|
Group {
|
||||||
?? (state.pitch == nil && state.roll == nil ? "Keine Messwerte" : attributes.deviceName))
|
if !hasReading {
|
||||||
.font(.callout.weight(.semibold))
|
Text("–")
|
||||||
.foregroundStyle(state.isLevel ? .green : .white)
|
} else if state.isLevel {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
if let pitchLetter { Text(pitchLetter) }
|
||||||
|
Image(systemName: "arrow.up")
|
||||||
|
if let rollLetter { Text(rollLetter) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skalierbarer Textstil statt fester Punktgrösse: CarPlay-
|
||||||
|
// Bildschirme unterscheiden sich in Grösse, die exakten Masse
|
||||||
|
// eines Displays sind aber nicht abfragbar. `minimumScaleFactor`
|
||||||
|
// lässt die Schrift so gross wie möglich, aber so klein wie
|
||||||
|
// nötig werden, um in den vom System zugeteilten Platz zu passen.
|
||||||
|
.font(.title2.weight(.bold))
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.8)
|
.foregroundStyle(statusColor)
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,20 +215,17 @@ private struct LockScreenLevelView: View {
|
|||||||
let attributes: LevelActivityAttributes
|
let attributes: LevelActivityAttributes
|
||||||
let state: LevelActivityAttributes.ContentState
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|
||||||
/// Muss mit `LevelState.levelTolerance` übereinstimmen.
|
|
||||||
private let tolerance: Double = 0.5
|
|
||||||
|
|
||||||
/// Längszeile, z.B. "Heck steht höher 1.8°" – nur wenn ausserhalb der
|
/// Längszeile, z.B. "Heck steht höher 1.8°" – nur wenn ausserhalb der
|
||||||
/// Toleranz, genau wie bei `LevelState.instruction` in der App.
|
/// Toleranz, genau wie bei `LevelState.instruction` in der App.
|
||||||
private var pitchLine: String? {
|
private var pitchLine: String? {
|
||||||
guard let pitch = state.pitch, abs(pitch) > tolerance else { return nil }
|
guard let pitch = state.pitch, abs(pitch) > levelTolerance else { return nil }
|
||||||
let label = pitch >= 0 ? "Heck steht höher" : "Front steht höher"
|
let label = pitch >= 0 ? "Heck steht höher" : "Front steht höher"
|
||||||
return "\(label) \(LevelDirectionFormatting.magnitude(pitch))"
|
return "\(label) \(LevelDirectionFormatting.magnitude(pitch))"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Querzeile, z.B. "links steht höher 0.9°".
|
/// Querzeile, z.B. "links steht höher 0.9°".
|
||||||
private var rollLine: String? {
|
private var rollLine: String? {
|
||||||
guard let roll = state.roll, abs(roll) > tolerance else { return nil }
|
guard let roll = state.roll, abs(roll) > levelTolerance else { return nil }
|
||||||
let label = roll >= 0 ? "rechts steht höher" : "links steht höher"
|
let label = roll >= 0 ? "rechts steht höher" : "links steht höher"
|
||||||
return "\(label) \(LevelDirectionFormatting.magnitude(roll))"
|
return "\(label) \(LevelDirectionFormatting.magnitude(roll))"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user