Flighty : Visualisation des données, bien faite
Pourquoi Flighty a remporté l'Apple Design Award 2023 : 15 états intelligents, Live Activities et visualisation de données inspirée de l'aéroport. Avec motifs d'implémentation SwiftUI.
Flighty : La visualisation de données bien faite
« Nous voulons que Flighty fonctionne si bien que cela semble presque évidemment banal. »
Flighty transforme des données de vol complexes en informations consultables d'un coup d'œil. Lauréat de l'Apple Design Award 2023 pour l'Interaction, c'est une masterclass dans la présentation de données denses à travers un design calme et clair, inspiré de décennies de signalétique aéroportuaire.
Pourquoi Flighty est important
Fondé par Ryan Jones, ancien employé d'Apple, après un retard de vol frustrant, Flighty est devenu la référence absolue pour la visualisation de données sur iOS.
Réalisations clés : - Apple Design Award 2023 (Interaction) - Finaliste de l'App iPhone de l'Année 2023 - Première application à exploiter pleinement les Live Activities - 15 états intelligents contextuels - Prédictions de retards leaders du secteur
Points clés à retenir
- S'inspirer de systèmes éprouvés - Les tableaux de départ d'aéroport ont plus de 50 ans d'optimisation ; Flighty adapte ce langage visuel pour le mobile
- Les états contextuels réduisent la charge cognitive - 15 états intelligents garantissent que les utilisateurs voient exactement ce dont ils ont besoin à chaque phase du voyage
- « Évidemment banal » est l'objectif - Si l'application fonctionne simplement sans friction, vous avez réussi
- Condensez, enveloppez et colorez vos données - Présentez des insights, pas des données brutes ; codez les statuts par couleur pour éviter aux utilisateurs d'interpréter
- Offline-first pour la connectivité peu fiable - Les vols passent hors ligne ; l'application doit fonctionner sans connexion, particulièrement en vol
Philosophie de design fondamentale
La signalétique aéroportuaire comme langage de design
Le design de Flighty repose sur une analogie du monde réel : les tableaux de départ d'aéroport.
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]
Insight clé : « Ces tableaux d'aéroport ont une ligne par vol, et c'est un bon fil conducteur — ils ont eu 50 ans pour déterminer ce qui est important. »
Conscience du contexte de voyage
Flighty comprend que voyager est stressant. Le design doit réduire la charge cognitive, pas l'augmenter.
Principes de design : 1. L'information la plus importante au-dessus de la ligne de flottaison 2. Divulgation progressive pour les détails 3. Statut codé par couleur (vert = bon, jaune = attention, rouge = problème) 4. Les données sont « condensées, enveloppées et colorées » pour éviter aux utilisateurs d'interpréter
Bibliothèque de patterns
1. Les 15 états intelligents
L'innovation signature de Flighty : 15 états contextuels qui montrent exactement ce dont vous avez besoin, exactement quand vous en avez besoin.
Chronologie des états :
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 [→] │
└─────────────────────────────────────────┘
Concept d'implémentation :
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 et Dynamic Island
Flighty a été la première application à exploiter pleinement les Live Activities d'iOS 16, établissant ainsi la référence pour cette fonctionnalité.
États du Dynamic Island :
COMPACT (Minimal)
╭──────────────────────────────────╮
│ [^] B22 │ * │ 2:30 -> 1h 45m │
╰──────────────────────────────────╯
Gate Status Time/Duration
EXPANDED (Long press)
╭────────────────────────────────────────╮
│ UA 1234 to San Francisco │
│ ─────────────────────────────────── │
│ Gate B22 Boards 2:00 PM │
│ Seat 14A On Time ✓ │
│ │
│ [Open Flighty] [Share ETA] │
╰────────────────────────────────────────╯
IN-FLIGHT (Offline capable)
╭──────────────────────────────────╮
│ SFO ●════════○──── JFK │
│ 2h 15m │
╰──────────────────────────────────╯
Pattern d'implémentation SwiftUI :
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 to 1.0 for in-flight
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
// Vue écran de verrouillage
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Vue étendue
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: {
// Côté gauche compact
Text(context.attributes.gate)
.font(.caption)
} compactTrailing: {
// Côté droit compact
Text(context.state.departureTime, style: .timer)
} minimal: {
// Minimal (quand plusieurs activités)
Image(systemName: "airplane")
}
}
}
}
3. Visualisation de la progression du vol
Flighty affiche la progression du vol à travers plusieurs métaphores visuelles.
Ligne de route avec indicateur d'avion :
PROGRESSION LINÉAIRE (Principale)
───────────────────────────────────────────────────────────────────
SFO ════════════════●════════════════════════════════════ JFK
[Décollé il y a 45min] ↑ Vous êtes ici [2h 15min restantes]
PROGRESSION CIRCULAIRE (Compacte)
╭───────╮
/ ● \
│ | │ ← L'arc de progression se remplit dans le sens horaire
│ | │
\ ↓ / 2h 15min restantes
╰───────╯
CARTE THERMIQUE (Statistiques globales)
[Carte avec des lignes de route d'épaisseurs variées]
Ligne épaisse = itinéraire fréquemment emprunté
Ligne fine = emprunté une seule fois
Implémentation SwiftUI :
struct FlightProgressView: View {
let progress: Double // 0.0 à 1.0
let origin: Airport
let destination: Airport
let remainingTime: TimeInterval
var body: some View {
GeometryReader { geometry in
ZStack {
// Ligne de route
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)
// Ligne de progression
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)
// Marqueur d'origine
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: 40, y: geometry.size.height / 2)
// Icône d'avion à la position actuelle
Image(systemName: "airplane")
.foregroundStyle(Color.accentColor)
.position(
x: 40 + (geometry.size.width - 80) * progress,
y: geometry.size.height / 2
)
// Marqueur de destination
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: geometry.size.width - 40, y: geometry.size.height / 2)
// Libellés
HStack {
Text(origin.code)
.font(.caption.bold())
Spacer()
Text(destination.code)
.font(.caption.bold())
}
.padding(.horizontal, 20)
}
}
.frame(height: 60)
}
}
4. Assistant de correspondance
Lors de l'atterrissage avec un vol en correspondance, Flighty affiche les deux portes avec votre position en temps réel.
VUE CORRESPONDANCE
┌─────────────────────────────────────────┐
│ Correspondance : 45 minutes │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ [Plan du terminal] │ │
│ │ │ │
│ │ [*] Vous -> -> -> -> -> [>] D22 │ │
│ │ C15 8 min à pied │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ [!] Correspondance serrée - partez │
└─────────────────────────────────────────┘
Implémentation :
struct ConnectionAssistantView: View {
let arrival: Flight
let departure: Flight
let userLocation: CLLocationCoordinate2D?
var connectionTime: TimeInterval {
departure.scheduledDeparture.timeIntervalSince(arrival.estimatedArrival)
}
var walkingTime: TimeInterval {
// Calcul basé sur la distance entre les portes et la vitesse de marche typique
TerminalMap.walkingTime(from: arrival.arrivalGate, to: departure.gate)
}
var isTight: Bool {
connectionTime < walkingTime + 15 * 60 // Moins de 15 min de marge
}
var body: some View {
VStack(spacing: 16) {
// En-tête
HStack {
Text("Correspondance")
.font(.headline)
Spacer()
Text(connectionTime.formatted())
.font(.title2.bold())
.foregroundStyle(isTight ? .orange : .primary)
}
// Carte avec itinéraire
TerminalMapView(
from: arrival.arrivalGate,
to: departure.gate,
userLocation: userLocation
)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
// Informations des portes
HStack {
GateLabel(gate: arrival.arrivalGate, label: "Arrivée")
Spacer()
Text("\(Int(walkingTime / 60)) min à pied")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
GateLabel(gate: departure.gate, label: "Départ")
}
// 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. Passeport et statistiques cumulées
Flighty transforme l'historique de voyage en visualisations élégantes et partageables.
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] │
└─────────────────────────────────────────┘
Système de design visuel
Palette de couleurs
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
}
Typographie
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)
}
Philosophie d'animation
Mouvement intentionnel
Les animations de Flighty servent une fonction, pas une décoration.
// 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
}
}
}
Enseignements pour notre travail
1. Les analogies du monde réel guident le design
La signalétique aéroportuaire bénéficie de plus de 50 ans d'optimisation. Inspirez-vous des systèmes éprouvés.
2. Les états contextuels réduisent la charge cognitive
15 états intelligents signifient que les utilisateurs ne voient jamais d'informations non pertinentes.
3. L'évidence ennuyeuse est l'objectif
Si l'application « fonctionne tout simplement », vous avez réussi. La friction est un échec.
4. Condensez, enveloppez, colorez vos données
N'affichez pas les données brutes. Présentez des insights. Utilisez des codes couleur pour les statuts.
5. Priorité au hors-ligne pour le mobile
Les vols passent hors ligne. L'application doit fonctionner sans connexion.
Questions fréquemment posées
Comment fonctionnent les 15 états intelligents de Flighty ?
Flighty surveille le statut de votre vol, votre position et l'heure pour afficher automatiquement les informations les plus pertinentes. Les états vont de « loin » (24+ heures avant) jusqu'à « direction l'aéroport », « à la porte », « embarquement », « en vol » et « atterri ». Chaque état fait apparaître différents détails : les numéros de confirmation en amont, les informations de porte quand c'est pertinent, le temps de vol restant pendant le trajet, et les indications de correspondance après l'atterrissage.
Qu'est-ce qui rend l'implémentation des Live Activities de Flighty si spéciale ?
Flighty a été la première application à exploiter pleinement les Live Activities et la Dynamic Island d'iOS 16. L'implémentation comprend des vues compactes affichant la porte et le compte à rebours, des vues étendues avec tous les détails du vol, et une visualisation de la progression en vol fonctionnant hors ligne. Le design traite les Live Activities comme une interface principale, pas comme une réflexion après coup.
Comment Flighty gère-t-il les scénarios hors ligne ?
Flighty télécharge les données de vol et les stocke localement avant le départ. La progression en vol continue en se basant sur les horaires prévus et les performances connues de l'avion. L'application pré-calcule les estimations d'arrivée, les temps de correspondance et les informations de porte. Quand la connectivité revient, les données se synchronisent et se mettent à jour avec les éventuels changements. Cette approche priorité hors-ligne est essentielle puisque les vols passent des heures sans internet.
Pourquoi Flighty utilise-t-il la signalétique aéroportuaire comme inspiration de design ?
Les tableaux d'affichage des départs dans les aéroports ont été perfectionnés pendant plus de 50 ans pour communiquer rapidement les informations de vol aux voyageurs stressés dans des environnements animés. Ils utilisent une ligne par vol, des statuts codés par couleur, et uniquement les informations essentielles. Flighty adapte ce langage visuel éprouvé au mobile, utilisant les mêmes principes de densité, clarté et indication de statut que les aéroports ont optimisés pendant des décennies.
Comment l'assistant de correspondance calcule-t-il les correspondances serrées ?
L'assistant de correspondance compare votre heure d'arrivée avec le prochain départ, puis calcule le temps de marche en fonction de la distance entre les portes au sein du terminal. Il prend en compte les vitesses de marche typiques et les configurations des terminaux. Si la marge tombe en dessous de 15 minutes, l'application signale une correspondance serrée et affiche une carte en direct avec votre position actuelle, la porte de destination et l'itinéraire à pied.