Flighty: Visualización de Datos Hecha Correctamente
Por qué Flighty ganó el Apple Design Award 2023: 15 estados inteligentes, Live Activities y visualización de datos inspirada en aeropuertos. Con patrones de implementación SwiftUI.
Flighty: Visualización de Datos Bien Hecha
“Queremos que Flighty funcione tan bien que se sienta casi obvio de tan natural.”
Flighty convierte datos complejos de vuelos en información que se comprende de un vistazo. Ganador del Apple Design Award 2023 en la categoría de Interacción, es una clase magistral en la presentación de datos densos a través de un diseño sereno y claro, inspirado en décadas de señalización aeroportuaria.
Por Qué Flighty Importa
Fundada por Ryan Jones, exempleado de Apple, tras una frustrante demora de vuelo, Flighty se ha convertido en el estándar de referencia para la visualización de datos en iOS.
Logros clave: - Apple Design Award 2023 (Interacción) - Finalista a App del Año para iPhone 2023 - Primera app en aprovechar completamente Live Activities - 15 estados inteligentes según el contexto - Predicciones de retrasos líderes en la industria
Conclusiones Clave
- Inspírate en sistemas probados - Los paneles de salidas de los aeropuertos tienen más de 50 años de optimización; Flighty adapta este lenguaje visual para dispositivos móviles
- Los estados según el contexto reducen la carga cognitiva - 15 estados inteligentes aseguran que los usuarios vean exactamente lo que necesitan en cada fase del viaje
- “Obvio de tan natural” es la meta - Si la app simplemente funciona sin fricción, has tenido éxito
- Empaqueta, envuelve y colorea tus datos - Presenta información procesada, no datos crudos; codifica el estado con colores para que los usuarios no tengan que interpretar
- Offline-first para conectividad poco confiable - Los vuelos pierden conexión; la app debe funcionar sin internet, especialmente durante el vuelo
Filosofía de Diseño Central
Señalización Aeroportuaria como Lenguaje de Diseño
El diseño de Flighty se construye sobre una analogía del mundo real: los paneles de salidas de los aeropuertos.
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]
Idea clave: “Esos paneles de aeropuerto tienen una línea por vuelo, y ese es un buen principio rector—han tenido 50 años para descifrar qué es lo importante.”
Conciencia del Contexto de Viaje
Flighty entiende que viajar es estresante. El diseño debe reducir la carga cognitiva, no aumentarla.
Principios de diseño: 1. La información más importante visible sin desplazar 2. Revelación progresiva para los detalles 3. Estado codificado por colores (verde = bien, amarillo = precaución, rojo = problema) 4. Los datos se “empaquetan, envuelven y colorean” para que los usuarios no tengan que interpretarlos
Biblioteca de Patrones
1. Los 15 Estados Inteligentes
La innovación distintiva de Flighty: 15 estados según el contexto que muestran exactamente lo que necesitas, exactamente cuando lo necesitas.
Línea temporal de estados:
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 [→] │
└─────────────────────────────────────────┘
Concepto de implementación:
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 y Dynamic Island
Flighty fue la primera app en aprovechar completamente las Live Activities de iOS 16, estableciendo el estándar para esta funcionalidad.
Estados de Dynamic Island:
COMPACT (Mínimo)
╭──────────────────────────────────╮
│ [^] B22 │ * │ 2:30 -> 1h 45m │
╰──────────────────────────────────╯
Gate Estado Hora/Duración
EXPANDED (Pulsación prolongada)
╭────────────────────────────────────────╮
│ UA 1234 a San Francisco │
│ ─────────────────────────────────── │
│ Gate B22 Embarque 2:00 PM │
│ Asiento 14A A tiempo ✓ │
│ │
│ [Abrir Flighty] [Compartir ETA] │
╰────────────────────────────────────────╯
IN-FLIGHT (Funciona sin conexión)
╭──────────────────────────────────╮
│ SFO ●════════○──── JFK │
│ 2h 15m │
╰──────────────────────────────────╯
Patrón de implementación en 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 a 1.0 para en vuelo
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
// Vista de pantalla de bloqueo
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Vista expandida
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: {
// Lado izquierdo compacto
Text(context.attributes.gate)
.font(.caption)
} compactTrailing: {
// Lado derecho compacto
Text(context.state.departureTime, style: .timer)
} minimal: {
// Mínimo (cuando hay múltiples actividades)
Image(systemName: "airplane")
}
}
}
}
3. Visualización del Progreso de Vuelo
Flighty muestra el progreso del vuelo a través de múltiples metáforas visuales.
Línea de ruta con indicador de avión:
PROGRESO LINEAL (Principal)
───────────────────────────────────────────────────────────────────
SFO ════════════════●════════════════════════════════════ JFK
[Despegó hace 45m] ↑ Estás aquí [2h 15m restantes]
PROGRESO CIRCULAR (Compacto)
╭───────╮
/ ● \
│ | │ ← El arco de progreso se llena en sentido horario
│ | │
\ ↓ / 2h 15m restantes
╰───────╯
MAPA DE CALOR (Estadísticas de por vida)
[Mapa con líneas de ruta de grosor variable]
Línea gruesa = ruta viajada frecuentemente
Línea delgada = viajada una sola vez
Implementación en SwiftUI:
struct FlightProgressView: View {
let progress: Double // 0.0 a 1.0
let origin: Airport
let destination: Airport
let remainingTime: TimeInterval
var body: some View {
GeometryReader { geometry in
ZStack {
// Línea de ruta
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)
// Línea de progreso
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. Asistente de Conexión
Al aterrizar con un vuelo de conexión, Flighty muestra ambas puertas de embarque junto con tu ubicación en tiempo real.
CONNECTION VIEW
┌─────────────────────────────────────────┐
│ Connection: 45 minutes │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ [Terminal Map] │ │
│ │ │ │
│ │ [*] You -> -> -> -> -> [>] D22 │ │
│ │ C15 8 min walk │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ [!] Tight connection - start moving │
└─────────────────────────────────────────┘
Implementación:
struct ConnectionAssistantView: View {
let arrival: Flight
let departure: Flight
let userLocation: CLLocationCoordinate2D?
var connectionTime: TimeInterval {
departure.scheduledDeparture.timeIntervalSince(arrival.estimatedArrival)
}
var walkingTime: TimeInterval {
// Calculate based on gate distance and typical walking speed
TerminalMap.walkingTime(from: arrival.arrivalGate, to: departure.gate)
}
var isTight: Bool {
connectionTime < walkingTime + 15 * 60 // Less than 15 min buffer
}
var body: some View {
VStack(spacing: 16) {
// Header
HStack {
Text("Connection")
.font(.headline)
Spacer()
Text(connectionTime.formatted())
.font(.title2.bold())
.foregroundStyle(isTight ? .orange : .primary)
}
// Map with route
TerminalMapView(
from: arrival.arrivalGate,
to: departure.gate,
userLocation: userLocation
)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
// Gate info
HStack {
GateLabel(gate: arrival.arrivalGate, label: "Arrival")
Spacer()
Text("\(Int(walkingTime / 60)) min walk")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
GateLabel(gate: departure.gate, label: "Departure")
}
// 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. Pasaporte y estadísticas de por vida
Flighty convierte el historial de viajes en visualizaciones hermosas y compartibles.
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] │
└─────────────────────────────────────────┘
Sistema de diseño visual
Paleta de colores
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
}
Tipografía
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)
}
Filosofía de animación
Movimiento con propósito
Las animaciones de Flighty cumplen una función, no son decorativas.
// 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
}
}
}
Lecciones para nuestro trabajo
1. Las analogías del mundo real guían el diseño
La señalización aeroportuaria cuenta con más de 50 años de optimización. Aprovecha sistemas ya probados.
2. Los estados contextuales reducen la carga cognitiva
15 estados inteligentes significan que los usuarios nunca ven información irrelevante.
3. Lo aburrido y obvio es el objetivo
Si la app “simplemente funciona”, has tenido éxito. La fricción es fracaso.
4. Empaqueta, envuelve y colorea tus datos
No muestres datos crudos. Presenta información procesada. Codifica el estado con colores.
5. Offline-first para móvil
Los vuelos se quedan sin conexión. La app debe funcionar sin conectividad.
Preguntas frecuentes
¿Cómo funcionan los 15 estados inteligentes de Flighty?
Flighty monitorea el estado de tu vuelo, tu ubicación y la hora para mostrar automáticamente la información más relevante. Los estados van desde “muy anticipado” (más de 24 horas antes) pasando por “dirigirse al aeropuerto”, “en la puerta”, “embarcando”, “en vuelo” y “aterrizado”. Cada estado muestra diferentes detalles: números de confirmación con anticipación, información de la puerta cuando es relevante, tiempo de vuelo restante mientras estás en el aire e indicaciones de conexión tras el aterrizaje.
¿Qué hace especial la implementación de Live Activities de Flighty?
Flighty fue la primera app en aprovechar completamente las Live Activities y la Dynamic Island de iOS 16. La implementación incluye vistas compactas que muestran la puerta y la cuenta regresiva, vistas expandidas con todos los detalles del vuelo y una visualización del progreso en vuelo compatible con el modo offline que funciona sin conectividad. El diseño trata las Live Activities como una interfaz principal, no como algo secundario.
¿Cómo maneja Flighty los escenarios sin conexión?
Flighty descarga los datos del vuelo y los almacena localmente antes de la salida. El progreso en vuelo continúa basándose en los horarios programados y el rendimiento conocido de la aeronave. La app precalcula estimaciones de llegada, tiempos de conexión e información de puertas. Cuando la conectividad regresa, los datos se sincronizan y actualizan con cualquier cambio. Este enfoque offline-first es esencial ya que los vuelos pasan horas sin internet.
¿Por qué Flighty usa la señalización aeroportuaria como inspiración de diseño?
Los paneles de salidas de los aeropuertos se han perfeccionado durante más de 50 años para comunicar información de vuelos rápidamente a viajeros estresados en entornos concurridos. Utilizan una línea por vuelo, estados codificados por color y solo la información esencial. Flighty adapta este lenguaje visual probado para dispositivos móviles, empleando los mismos principios de densidad, claridad e indicación de estado que los aeropuertos han optimizado durante décadas.
¿Cómo calcula el asistente de conexiones las conexiones ajustadas?
El asistente de conexiones compara tu hora de llegada con la siguiente salida y luego calcula el tiempo de caminata según la distancia entre puertas dentro de la terminal. Tiene en cuenta velocidades típicas de caminata y la distribución de la terminal. Si el margen baja de 15 minutos, la app lo marca como conexión ajustada y muestra un mapa en vivo con tu ubicación actual, la puerta de destino y la ruta a pie.