Flighty: Datenvisualisierung richtig gemacht
Warum Flighty den Apple Design Award 2023 gewann: 15 Smart States, Live Activities und flughafen-inspirierte Datenvisualisierung. Mit SwiftUI-Implementierungsmustern.
Flighty: Datenvisualisierung richtig gemacht
„Wir möchten, dass Flighty so gut funktioniert, dass es fast schon langweilig offensichtlich wirkt."
Flighty verwandelt komplexe Flugdaten in auf einen Blick erfassbare Informationen. Als Gewinner des Apple Design Award 2023 in der Kategorie Interaktion ist es ein Paradebeispiel dafür, wie man dichte Daten durch ruhiges, klares Design präsentiert – inspiriert von Jahrzehnten der Flughafen-Beschilderung.
Warum Flighty wichtig ist
Gegründet vom ehemaligen Apple-Mitarbeiter Ryan Jones nach einer frustrierenden Flugverspätung, ist Flighty zum Goldstandard für iOS-Datenvisualisierung geworden.
Wichtige Errungenschaften: - Apple Design Award 2023 (Interaktion) - Finalist iPhone App des Jahres 2023 - Erste App mit vollständiger Nutzung von Live Activities - 15 kontextbewusste Smart States - Branchenführende Verspätungsvorhersagen
Wichtigste Erkenntnisse
- Von bewährten Systemen lernen – Flughafen-Abflugtafeln wurden über 50 Jahre optimiert; Flighty adaptiert diese visuelle Sprache für Mobilgeräte
- Kontextbewusste Zustände reduzieren kognitive Belastung – 15 Smart States stellen sicher, dass Nutzer in jeder Reisephase genau das sehen, was sie brauchen
- „Langweilig offensichtlich" ist das Ziel – Wenn die App einfach reibungslos funktioniert, haben Sie es geschafft
- Daten verdichten, aufbereiten und farblich kennzeichnen – Erkenntnisse präsentieren, keine Rohdaten; Status farblich kodieren, damit Nutzer nicht interpretieren müssen
- Offline-First für unzuverlässige Verbindungen – Flüge verlieren die Verbindung; die App muss ohne Internet funktionieren, besonders während des Fluges
Grundlegende Designphilosophie
Flughafen-Beschilderung als Designsprache
Flightys Design basiert auf einer realen Analogie: Flughafen-Abflugtafeln.
AIRPORT DEPARTURE BOARD (50+ years of refinement)
───────────────────────────────────────────────────────────────────
FLIGHT DESTINATION GATE TIME STATUS
UA 1234 San Francisco B22 14:30 ON TIME
DL 567 New York JFK C15 15:00 DELAYED
AA 890 Chicago O'Hare A08 15:45 BOARDING
One line per flight. Essential info only. Decades of optimization.
FLIGHTY'S TRANSLATION
───────────────────────────────────────────────────────────────────
[Flight card with same information density]
[Color-coded status]
[Time-appropriate details]
Zentrale Erkenntnis: „Diese Flughafentafeln haben eine Zeile pro Flug, und das ist ein guter Leitfaden – sie hatten 50 Jahre Zeit herauszufinden, was wichtig ist."
Kontextbewusstsein für Reisen
Flighty versteht, dass Reisen stressig ist. Das Design muss die kognitive Belastung reduzieren, nicht erhöhen.
Designprinzipien: 1. Wichtigste Informationen above the fold 2. Progressive Disclosure für Details 3. Farbkodierter Status (Grün = gut, Gelb = Warnung, Rot = Problem) 4. Daten werden „verdichtet, aufbereitet und eingefärbt", damit Nutzer nicht interpretieren müssen
Pattern-Bibliothek
1. Die 15 Smart States
Flightys charakteristische Innovation: 15 kontextbewusste Zustände, die genau zeigen, was Sie brauchen, genau dann, wenn Sie es brauchen.
Zeitlicher Ablauf der Zustände:
24 HOURS BEFORE
┌─────────────────────────────────────────┐
│ UA 1234 → SFO │
│ Tomorrow at 2:30 PM │
│ Confirmation: ABC123 │
│ Head to Terminal 2 │
└─────────────────────────────────────────┘
3 HOURS BEFORE
┌─────────────────────────────────────────┐
│ UA 1234 → SFO │
│ Gate B22 • Boards 2:00 PM │
│ Leave for airport by 12:15 PM │
│ [View full timeline →] │
└─────────────────────────────────────────┘
AT GATE
┌─────────────────────────────────────────┐
│ UA 1234 → SFO [ON TIME] │
│ Gate B22 • Boarding in 12 min │
│ Seat 14A • Window │
│ [Your plane is here ✓] │
└─────────────────────────────────────────┘
WALKING TO PLANE
┌─────────────────────────────────────────┐
│ UA 1234 → SFO [BOARDING] │
│ Your seat: 14A (Window) │
│ ▓▓▓▓▓▓▓▓░░ 80% boarded │
└─────────────────────────────────────────┘
IN FLIGHT (offline)
┌─────────────────────────────────────────┐
│ SFO ──────●───────────── JFK │
│ │ │
│ 2h 15m remaining │
│ Arrive ~5:45 PM local │
└─────────────────────────────────────────┘
LANDED
┌─────────────────────────────────────────┐
│ ✓ Arrived at JFK │
│ Your gate: C15 → Connection gate: D22 │
│ 8 min walk • Terminal map [→] │
└─────────────────────────────────────────┘
Implementierungskonzept:
enum FlightState: CaseIterable {
case farOut // > 24 hours
case dayBefore // 24 hours
case headToAirport // ~3 hours
case atAirport // At terminal
case atGate // Gate area
case boarding // Boarding started
case onBoard // Seated
case taxiing // Moving to runway
case inFlight // Airborne
case descending // Approaching
case landed // Touched down
case atGateDest // Arrived at gate
case connection // Connecting flight
case completed // Trip done
case delayed // Any delay state
}
struct SmartStateEngine {
func currentState(for flight: Flight, context: TravelContext) -> FlightState {
let now = Date()
let departure = flight.scheduledDeparture
// Contextual factors
let isAtAirport = context.currentLocation.isNear(flight.departureAirport)
let isBoarding = flight.boardingStatus == .active
let isAirborne = flight.status == .inFlight
switch true {
case isAirborne:
return flight.altitude > 10000 ? .inFlight : .taxiing
case isBoarding && isAtAirport:
return .boarding
case isAtAirport && departure.timeIntervalSinceNow < 3600:
return .atGate
case isAtAirport:
return .atAirport
case departure.timeIntervalSinceNow < 3 * 3600:
return .headToAirport
case departure.timeIntervalSinceNow < 24 * 3600:
return .dayBefore
default:
return .farOut
}
}
func displayContent(for state: FlightState, flight: Flight) -> StateContent {
switch state {
case .headToAirport:
return StateContent(
headline: "\(flight.number) → \(flight.destination.code)",
primary: "Gate \(flight.gate) • Boards \(flight.boardingTime.formatted())",
secondary: "Leave for airport by \(flight.recommendedDeparture.formatted())",
action: "View full timeline"
)
case .inFlight:
return StateContent(
headline: flight.routeVisualization,
primary: "\(flight.remainingTime.formatted()) remaining",
secondary: "Arrive ~\(flight.estimatedArrival.formatted())",
action: nil
)
// ... other states
}
}
}
2. Live Activities & Dynamic Island
Flighty war die erste App, die iOS 16s Live Activities vollständig nutzte und damit den Standard für diese Funktion setzte.
Dynamic Island-Zustände:
COMPACT (Minimal)
╭──────────────────────────────────╮
│ [^] B22 │ * │ 2:30 -> 1h 45m │
╰──────────────────────────────────╯
Gate Status Zeit/Dauer
EXPANDED (Langes Drücken)
╭────────────────────────────────────────╮
│ UA 1234 nach San Francisco │
│ ─────────────────────────────────── │
│ Gate B22 Boarding 2:00 PM │
│ Sitz 14A Pünktlich ✓ │
│ │
│ [Flighty öffnen] [ETA teilen] │
╰────────────────────────────────────────╯
IN-FLIGHT (Offline-fähig)
╭──────────────────────────────────╮
│ SFO ●════════○──── JFK │
│ 2h 15m │
╰──────────────────────────────────╯
SwiftUI-Implementierungsmuster:
import ActivityKit
import WidgetKit
struct FlightActivityAttributes: ActivityAttributes {
public typealias ContentState = FlightStatus
let flightNumber: String
let origin: Airport
let destination: Airport
let gate: String
let seat: String
struct FlightStatus: Codable, Hashable {
let departureTime: Date
let arrivalTime: Date
let status: Status
let boardingStatus: BoardingStatus
let progress: Double // 0.0 bis 1.0 für den Flug
let phase: FlightPhase
enum Status: String, Codable { case onTime, delayed, cancelled }
enum BoardingStatus: String, Codable { case notStarted, boarding, closed }
enum FlightPhase: String, Codable { case preBoarding, boarding, taxiing, inFlight, landed }
}
}
struct FlightLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: FlightActivityAttributes.self) { context in
// Lock Screen view
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Expanded view
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading) {
Text(context.attributes.flightNumber)
.font(.headline)
Text("to \(context.attributes.destination.city)")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing) {
Text("Gate \(context.attributes.gate)")
Text(context.state.status.displayText)
.foregroundStyle(context.state.status.color)
}
}
DynamicIslandExpandedRegion(.bottom) {
if context.state.phase == .inFlight {
FlightProgressBar(progress: context.state.progress)
} else {
Text("Boards \(context.state.boardingTime.formatted())")
}
}
} compactLeading: {
// Compact left side
Text(context.attributes.gate)
.font(.caption)
} compactTrailing: {
// Compact right side
Text(context.state.departureTime, style: .timer)
} minimal: {
// Minimal (when multiple activities)
Image(systemName: "airplane")
}
}
}
}
3. Flugfortschritts-Visualisierung
Flighty zeigt den Flugfortschritt durch mehrere visuelle Metaphern an.
Routenlinie mit Flugzeug-Indikator:
LINEAR PROGRESS (Primary)
───────────────────────────────────────────────────────────────────
SFO ════════════════●════════════════════════════════════ JFK
[Departed 45m ago] ↑ You are here [2h 15m remaining]
CIRCULAR PROGRESS (Compact)
╭───────╮
/ ● \
│ | │ ← Progress arc fills clockwise
│ | │
\ ↓ / 2h 15m remaining
╰───────╯
HEAT MAP (Lifetime Stats)
[Map with route lines of varying thickness]
Thick line = frequently traveled route
Thin line = traveled once
SwiftUI-Implementierung:
struct FlightProgressView: View {
let progress: Double // 0.0 to 1.0
let origin: Airport
let destination: Airport
let remainingTime: TimeInterval
var body: some View {
GeometryReader { geometry in
ZStack {
// Route line
Path { path in
let startX: CGFloat = 40
let endX = geometry.size.width - 40
let y = geometry.size.height / 2
path.move(to: CGPoint(x: startX, y: y))
path.addLine(to: CGPoint(x: endX, y: y))
}
.stroke(Color.secondary.opacity(0.3), lineWidth: 3)
// Progress line
Path { path in
let startX: CGFloat = 40
let endX = geometry.size.width - 40
let progressX = startX + (endX - startX) * progress
let y = geometry.size.height / 2
path.move(to: CGPoint(x: startX, y: y))
path.addLine(to: CGPoint(x: progressX, y: y))
}
.stroke(Color.accentColor, lineWidth: 3)
// Origin marker
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: 40, y: geometry.size.height / 2)
// Plane icon at current position
Image(systemName: "airplane")
.foregroundStyle(Color.accentColor)
.position(
x: 40 + (geometry.size.width - 80) * progress,
y: geometry.size.height / 2
)
// Destination marker
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: geometry.size.width - 40, y: geometry.size.height / 2)
// Labels
HStack {
Text(origin.code)
.font(.caption.bold())
Spacer()
Text(destination.code)
.font(.caption.bold())
}
.padding(.horizontal, 20)
}
}
.frame(height: 60)
}
}
4. Umsteigeassistent
Bei der Landung mit einem Anschlussflug zeigt Flighty beide Gates mit Ihrem Live-Standort an.
UMSTEIGEANSICHT
┌─────────────────────────────────────────┐
│ Umsteigezeit: 45 Minuten │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ [Terminalkarte] │ │
│ │ │ │
│ │ [*] Sie -> -> -> -> -> [>] D22 │ │
│ │ C15 8 Min Fußweg │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ [!] Knappe Umsteigezeit - bitte losgehen│
└─────────────────────────────────────────┘
Implementierung:
struct ConnectionAssistantView: View {
let arrival: Flight
let departure: Flight
let userLocation: CLLocationCoordinate2D?
var connectionTime: TimeInterval {
departure.scheduledDeparture.timeIntervalSince(arrival.estimatedArrival)
}
var walkingTime: TimeInterval {
// Berechnung basierend auf Gate-Entfernung und typischer Gehgeschwindigkeit
TerminalMap.walkingTime(from: arrival.arrivalGate, to: departure.gate)
}
var isTight: Bool {
connectionTime < walkingTime + 15 * 60 // Weniger als 15 Min Puffer
}
var body: some View {
VStack(spacing: 16) {
// Kopfzeile
HStack {
Text("Umsteigezeit")
.font(.headline)
Spacer()
Text(connectionTime.formatted())
.font(.title2.bold())
.foregroundStyle(isTight ? .orange : .primary)
}
// Karte mit Route
TerminalMapView(
from: arrival.arrivalGate,
to: departure.gate,
userLocation: userLocation
)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
// Gate-Informationen
HStack {
GateLabel(gate: arrival.arrivalGate, label: "Ankunft")
Spacer()
Text("\(Int(walkingTime / 60)) Min Fußweg")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
GateLabel(gate: departure.gate, label: "Abflug")
}
// Warning if tight
if isTight {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
Text("Tight connection - start moving")
.font(.callout)
}
.padding()
.background(Color.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.padding()
}
}
5. Passport & Lebenslang-Statistiken
Flighty verwandelt die Reisehistorie in ansprechende, teilbare Visualisierungen.
PASSPORT PAGE
┌─────────────────────────────────────────┐
│ [^] FLIGHTY PASSPORT │
│ │
│ [Custom artwork with travel themes] │
│ │
│ ─────────────────────────────────── │
│ 2024 │
│ │
│ 47 flights │
│ 89,234 miles │
│ 23 airports │
│ 12 countries │
│ │
│ Most visited: SFO (23 times) │
│ Longest flight: SFO → SIN (8,447 mi) │
│ ─────────────────────────────────── │
│ │
│ [Share] [View Details] │
└─────────────────────────────────────────┘
Visuelles Design-System
Farbpalette
extension Color {
// Status colors (airport-inspired)
static let flightOnTime = Color(hex: "#10B981") // Green
static let flightDelayed = Color(hex: "#F59E0B") // Amber
static let flightCancelled = Color(hex: "#EF4444") // Red
static let flightBoarding = Color(hex: "#3B82F6") // Blue
// UI colors
static let flightPrimary = Color(hex: "#1F2937")
static let flightSecondary = Color(hex: "#6B7280")
static let flightBackground = Color(hex: "#F9FAFB")
static let flightCard = Color(hex: "#FFFFFF")
// Accent
static let flightAccent = Color(hex: "#6366F1") // Indigo
}
Typografie
struct FlightyTypography {
// Flight numbers and gates (need to stand out)
static let flightNumber = Font.system(size: 20, weight: .bold, design: .monospaced)
static let gate = Font.system(size: 24, weight: .bold, design: .rounded)
// Time displays
static let time = Font.system(size: 18, weight: .medium, design: .monospaced)
static let countdown = Font.system(size: 32, weight: .bold, design: .monospaced)
// Status and labels
static let status = Font.system(size: 14, weight: .semibold)
static let label = Font.system(size: 12, weight: .medium)
static let body = Font.system(size: 16, weight: .regular)
}
Animationsphilosophie
Zweckmäßige Bewegung
Flightys Animationen dienen der Funktion, nicht der Dekoration.
// Status change animation
struct StatusBadge: View {
let status: FlightStatus
var body: some View {
Text(status.displayText)
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(status.color)
.foregroundStyle(.white)
.clipShape(Capsule())
.contentTransition(.numericText()) // Smooth number changes
.animation(.spring(response: 0.3), value: status)
}
}
// Progress bar animation
struct AnimatedProgress: View {
let progress: Double
var body: some View {
GeometryReader { geo in
Rectangle()
.fill(Color.accentColor)
.frame(width: geo.size.width * progress)
.animation(.linear(duration: 60), value: progress) // Slow, continuous
}
}
}
Erkenntnisse für unsere Arbeit
1. Reale Analogien leiten das Design
Flughafenbeschilderung wurde über 50+ Jahre optimiert. Leihen Sie von bewährten Systemen.
2. Kontextbewusste Zustände reduzieren die kognitive Belastung
15 intelligente Zustände bedeuten, dass Nutzer nie irrelevante Informationen sehen.
3. Langweilig Offensichtlich ist das Ziel
Wenn die App “einfach funktioniert”, haben Sie Erfolg gehabt. Reibung ist Versagen.
4. Packen, Verpacken, Färben Sie Ihre Daten
Zeigen Sie keine Rohdaten. Präsentieren Sie Erkenntnisse. Kennzeichnen Sie Status farblich.
5. Offline-First für Mobile
Flüge gehen offline. Die App muss ohne Verbindung funktionieren.
Häufig gestellte Fragen
Wie funktionieren Flightys 15 intelligente Zustände?
Flighty überwacht Ihren Flugstatus, Standort und die Zeit, um automatisch die relevantesten Informationen anzuzeigen. Die Zustände reichen von “weit entfernt” (24+ Stunden vorher) über “zum Flughafen aufbrechen”, “am Gate”, “Boarding”, “im Flug” bis “gelandet”. Jeder Zustand zeigt unterschiedliche Details: Buchungsnummern früh, Gate-Informationen wenn relevant, verbleibende Flugzeit während des Flugs und Anschlusshinweise nach der Landung.
Was macht Flightys Live Activities-Implementierung besonders?
Flighty war die erste App, die iOS 16s Live Activities und Dynamic Island vollständig nutzte. Die Implementierung umfasst kompakte Ansichten mit Gate und Countdown, erweiterte Ansichten mit vollständigen Flugdetails und eine offline-fähige Flugfortschrittsvisualisierung, die ohne Konnektivität funktioniert. Das Design behandelt Live Activities als primäre Schnittstelle, nicht als Nachgedanken.
Wie handhabt Flighty Offline-Szenarien?
Flighty lädt Flugdaten herunter und speichert sie vor dem Abflug lokal. Der Flugfortschritt wird basierend auf geplanten Zeiten und bekannter Flugzeugleistung fortgesetzt. Die App berechnet Ankunftsschätzungen, Anschlusszeiten und Gate-Informationen im Voraus. Wenn die Verbindung zurückkehrt, werden Daten synchronisiert und mit Änderungen aktualisiert. Dieser Offline-First-Ansatz ist essentiell, da Flüge Stunden ohne Internet verbringen.
Warum nutzt Flighty Flughafenbeschilderung als Design-Inspiration?
Flughafen-Abflugtafeln wurden über 50+ Jahre verfeinert, um Fluginformationen schnell an gestresste Reisende in geschäftigen Umgebungen zu kommunizieren. Sie verwenden eine Zeile pro Flug, farbcodierten Status und nur wesentliche Informationen. Flighty adaptiert diese bewährte visuelle Sprache für Mobile und nutzt dieselben Prinzipien von Dichte, Klarheit und Statusanzeige, die Flughäfen über Jahrzehnte optimiert haben.
Wie berechnet der Anschluss-Assistent knappe Verbindungen?
Der Anschluss-Assistent vergleicht Ihre Ankunftszeit mit dem nächsten Abflug und berechnet dann die Gehzeit basierend auf der Gate-Entfernung innerhalb des Terminals. Er berücksichtigt typische Gehgeschwindigkeiten und Terminal-Layouts. Wenn der Puffer unter 15 Minuten fällt, markiert die App dies als knappe Verbindung und zeigt eine Live-Karte mit Ihrem aktuellen Standort, Ziel-Gate und Fußweg an.