Flighty: Datenvisualisierung richtig gemacht

Warum Flighty den Apple Design Award 2023 gewann: 15 Smart States, Live Activities und flughafen-inspirierte Datenvisualisierung. Mit SwiftUI-Implementierungsmustern.

5 Min. Lesezeit 812 Worter
Flighty: Datenvisualisierung richtig gemacht screenshot

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

  1. Von bewährten Systemen lernen – Flughafen-Abflugtafeln wurden über 50 Jahre optimiert; Flighty adaptiert diese visuelle Sprache für Mobilgeräte
  2. Kontextbewusste Zustände reduzieren kognitive Belastung – 15 Smart States stellen sicher, dass Nutzer in jeder Reisephase genau das sehen, was sie brauchen
  3. „Langweilig offensichtlich" ist das Ziel – Wenn die App einfach reibungslos funktioniert, haben Sie es geschafft
  4. Daten verdichten, aufbereiten und farblich kennzeichnen – Erkenntnisse präsentieren, keine Rohdaten; Status farblich kodieren, damit Nutzer nicht interpretieren müssen
  5. 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.