Flighty: Visualização de Dados Feita da Forma Certa
Como o Flighty ganhou o Apple Design Award 2023: 15 estados inteligentes, Live Activities e visualização de dados inspirada em aeroportos. Com padrões de implementação em SwiftUI.
Flighty: Visualização de Dados Bem Feita
“Queremos que o Flighty funcione tão bem que pareça quase obviamente simples.”
O Flighty transforma dados complexos de voos em informações fáceis de visualizar. Vencedor do Apple Design Award 2023 na categoria Interação, é uma aula magistral em apresentar dados densos através de design calmo e claro, inspirado em décadas de sinalização aeroportuária.
Por Que o Flighty Importa
Fundado pelo ex-funcionário da Apple Ryan Jones após um atraso de voo frustrante, o Flighty se tornou o padrão ouro para visualização de dados no iOS.
Principais conquistas: - Apple Design Award 2023 (Interação) - Finalista do App do Ano para iPhone 2023 - Primeiro app a aproveitar totalmente as Live Activities - 15 estados inteligentes baseados em contexto - Previsões de atraso líderes do setor
Principais Conclusões
- Aprenda com sistemas comprovados - Painéis de embarque de aeroportos têm mais de 50 anos de otimização; o Flighty adapta essa linguagem visual para dispositivos móveis
- Estados baseados em contexto reduzem a carga cognitiva - 15 estados inteligentes garantem que os usuários vejam exatamente o que precisam em cada fase da viagem
- “Obviamente simples” é o objetivo - Se o app simplesmente funciona sem atrito, você teve sucesso
- Empacote, organize e colora seus dados - Apresente insights, não dados brutos; codifique por cores o status para que os usuários não precisem interpretar
- Offline-first para conectividade instável - Voos ficam offline; o app deve funcionar sem conexão, especialmente durante o voo
Filosofia Central de Design
Sinalização Aeroportuária como Linguagem de Design
O design do Flighty é construído sobre uma analogia do mundo real: painéis de embarque de aeroportos.
PAINEL DE EMBARQUE DO AEROPORTO (50+ anos de refinamento)
───────────────────────────────────────────────────────────────────
VOO DESTINO PORTÃO HORÁRIO STATUS
UA 1234 San Francisco B22 14:30 NO HORÁRIO
DL 567 New York JFK C15 15:00 ATRASADO
AA 890 Chicago O'Hare A08 15:45 EMBARQUE
Uma linha por voo. Apenas informações essenciais. Décadas de otimização.
TRADUÇÃO DO FLIGHTY
───────────────────────────────────────────────────────────────────
[Cartão de voo com mesma densidade de informação]
[Status codificado por cores]
[Detalhes apropriados para o momento]
Insight principal: “Aqueles painéis de aeroporto têm uma linha por voo, e esse é um bom princípio norteador—eles tiveram 50 anos para descobrir o que é importante.”
Consciência do Contexto de Viagem
O Flighty entende que viajar é estressante. O design deve reduzir a carga cognitiva, não aumentá-la.
Princípios de design: 1. Informações mais importantes acima da dobra 2. Revelação progressiva para detalhes 3. Status codificado por cores (verde = bom, amarelo = atenção, vermelho = problema) 4. Dados são “empacotados, organizados e coloridos” para que os usuários não precisem interpretar
Biblioteca de Padrões
1. Os 15 Estados Inteligentes
A inovação característica do Flighty: 15 estados baseados em contexto que mostram exatamente o que você precisa, exatamente quando você precisa.
Linha do tempo dos estados:
24 HORAS ANTES
┌─────────────────────────────────────────┐
│ UA 1234 → SFO │
│ Amanhã às 14:30 │
│ Confirmação: ABC123 │
│ Vá para o Terminal 2 │
└─────────────────────────────────────────┘
3 HORAS ANTES
┌─────────────────────────────────────────┐
│ UA 1234 → SFO │
│ Portão B22 • Embarque 14:00 │
│ Saia para o aeroporto às 12:15 │
│ [Ver cronograma completo →] │
└─────────────────────────────────────────┘
NO PORTÃO
┌─────────────────────────────────────────┐
│ UA 1234 → SFO [NO HORÁRIO] │
│ Portão B22 • Embarque em 12 min │
│ Assento 14A • Janela │
│ [Seu avião está aqui ✓] │
└─────────────────────────────────────────┘
CAMINHANDO PARA O AVIÃO
┌─────────────────────────────────────────┐
│ UA 1234 → SFO [EMBARCANDO] │
│ Seu assento: 14A (Janela) │
│ ▓▓▓▓▓▓▓▓░░ 80% embarcado │
└─────────────────────────────────────────┘
EM VOO (offline)
┌─────────────────────────────────────────┐
│ SFO ──────●───────────── JFK │
│ │ │
│ 2h 15m restantes │
│ Chegada ~17:45 horário local │
└─────────────────────────────────────────┘
POUSADO
┌─────────────────────────────────────────┐
│ ✓ Chegou ao JFK │
│ Seu portão: C15 → Portão conexão: D22 │
│ 8 min caminhando • Mapa do terminal [→] │
└─────────────────────────────────────────┘
Conceito de implementação:
enum FlightState: CaseIterable {
case farOut // > 24 horas
case dayBefore // 24 horas
case headToAirport // ~3 horas
case atAirport // No terminal
case atGate // Área do portão
case boarding // Embarque iniciado
case onBoard // Sentado
case taxiing // Indo para pista
case inFlight // No ar
case descending // Aproximando
case landed // Pousou
case atGateDest // Chegou ao portão
case connection // Voo de conexão
case completed // Viagem concluída
case delayed // Qualquer estado de atraso
}
struct SmartStateEngine {
func currentState(for flight: Flight, context: TravelContext) -> FlightState {
let now = Date()
let departure = flight.scheduledDeparture
// Fatores contextuais
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: "Portão \(flight.gate) • Embarque \(flight.boardingTime.formatted())",
secondary: "Saia para o aeroporto às \(flight.recommendedDeparture.formatted())",
action: "Ver cronograma completo"
)
case .inFlight:
return StateContent(
headline: flight.routeVisualization,
primary: "\(flight.remainingTime.formatted()) restantes",
secondary: "Chegada ~\(flight.estimatedArrival.formatted())",
action: nil
)
// ... outros estados
}
}
}
2. Live Activities e Dynamic Island
O Flighty foi o primeiro app a aproveitar totalmente as Live Activities do iOS 16, estabelecendo o padrão para o recurso.
Estados da Dynamic Island:
COMPACTO (Mínimo)
╭──────────────────────────────────╮
│ [^] B22 │ * │ 14:30 -> 1h 45m│
╰──────────────────────────────────╯
Portão Status Horário/Duração
EXPANDIDO (Pressão longa)
╭────────────────────────────────────────╮
│ UA 1234 para San Francisco │
│ ─────────────────────────────────── │
│ Portão B22 Embarque 14:00 │
│ Assento 14A No Horário ✓ │
│ │
│ [Abrir Flighty] [Compartilhar ETA] │
╰────────────────────────────────────────╯
EM VOO (Funciona offline)
╭──────────────────────────────────╮
│ SFO ●════════○──── JFK │
│ 2h 15m │
╰──────────────────────────────────╯
Padrão de implementação 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 em voo
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
// Visualização da Tela de Bloqueio
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// Visualização expandida
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading) {
Text(context.attributes.flightNumber)
.font(.headline)
Text("para \(context.attributes.destination.city)")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing) {
Text("Portão \(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("Embarque \(context.state.boardingTime.formatted())")
}
}
} compactLeading: {
// Lado esquerdo compacto
Text(context.attributes.gate)
.font(.caption)
} compactTrailing: {
// Lado direito compacto
Text(context.state.departureTime, style: .timer)
} minimal: {
// Mínimo (quando há múltiplas atividades)
Image(systemName: "airplane")
}
}
}
}
3. Visualização do Progresso do Voo
O Flighty mostra o progresso do voo através de múltiplas metáforas visuais.
Linha de rota com indicador de avião:
PROGRESSO LINEAR (Principal)
───────────────────────────────────────────────────────────────────
SFO ════════════════●════════════════════════════════════ JFK
[Decolou há 45m] ↑ Você está aqui [2h 15m restantes]
PROGRESSO CIRCULAR (Compacto)
╭───────╮
/ ● \
│ | │ ← Arco de progresso preenche em sentido horário
│ | │
\ ↓ / 2h 15m restantes
╰───────╯
MAPA DE CALOR (Estatísticas de Vida)
[Mapa com linhas de rota de espessuras variadas]
Linha grossa = rota frequentemente viajada
Linha fina = viajou uma vez
Implementação 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 {
// Linha da rota
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)
// Linha de progresso
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)
// Marcador de origem
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: 40, y: geometry.size.height / 2)
// Ícone do avião na posição atual
Image(systemName: "airplane")
.foregroundStyle(Color.accentColor)
.position(
x: 40 + (geometry.size.width - 80) * progress,
y: geometry.size.height / 2
)
// Marcador de destino
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.position(x: geometry.size.width - 40, y: geometry.size.height / 2)
// Rótulos
HStack {
Text(origin.code)
.font(.caption.bold())
Spacer()
Text(destination.code)
.font(.caption.bold())
}
.padding(.horizontal, 20)
}
}
.frame(height: 60)
}
}
4. Assistente de Conexão
Ao pousar com um voo de conexão, o Flighty mostra ambos os portões com sua localização ao vivo.
VISUALIZAÇÃO DE CONEXÃO
┌─────────────────────────────────────────┐
│ Conexão: 45 minutos │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ [Mapa do Terminal] │ │
│ │ │ │
│ │ [*] Você -> -> -> -> -> [>] D22 │ │
│ │ C15 8 min caminhando │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ [!] Conexão apertada - comece a andar │
└─────────────────────────────────────────┘
Implementação:
struct ConnectionAssistantView: View {
let arrival: Flight
let departure: Flight
let userLocation: CLLocationCoordinate2D?
var connectionTime: TimeInterval {
departure.scheduledDeparture.timeIntervalSince(arrival.estimatedArrival)
}
var walkingTime: TimeInterval {
// Calcula baseado na distância do portão e velocidade típica de caminhada
TerminalMap.walkingTime(from: arrival.arrivalGate, to: departure.gate)
}
var isTight: Bool {
connectionTime < walkingTime + 15 * 60 // Menos de 15 min de margem
}
var body: some View {
VStack(spacing: 16) {
// Cabeçalho
HStack {
Text("Conexão")
.font(.headline)
Spacer()
Text(connectionTime.formatted())
.font(.title2.bold())
.foregroundStyle(isTight ? .orange : .primary)
}
// Mapa com rota
TerminalMapView(
from: arrival.arrivalGate,
to: departure.gate,
userLocation: userLocation
)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
// Informações do portão
HStack {
GateLabel(gate: arrival.arrivalGate, label: "Chegada")
Spacer()
Text("\(Int(walkingTime / 60)) min caminhando")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
GateLabel(gate: departure.gate, label: "Partida")
}
// Aviso se apertado
if isTight {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
Text("Conexão apertada - comece a andar")
.font(.callout)
}
.padding()
.background(Color.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.padding()
}
}
5. Passaporte e Estatísticas de Vida
O Flighty transforma o histórico de viagens em visualizações bonitas e compartilháveis.
PÁGINA DO PASSAPORTE
┌─────────────────────────────────────────┐
│ [^] PASSAPORTE FLIGHTY │
│ │
│ [Arte personalizada com temas de viagem]│
│ │
│ ─────────────────────────────────── │
│ 2024 │
│ │
│ 47 voos │
│ 143.650 km │
│ 23 aeroportos │
│ 12 países │
│ │
│ Mais visitado: SFO (23 vezes) │
│ Voo mais longo: SFO → SIN (13.595 km) │
│ ─────────────────────────────────── │
│ │
│ [Compartilhar] [Ver Detalhes] │
└─────────────────────────────────────────┘
Sistema de Design Visual
Paleta de Cores
extension Color {
// Cores de status (inspiradas em aeroportos)
static let flightOnTime = Color(hex: "#10B981") // Verde
static let flightDelayed = Color(hex: "#F59E0B") // Âmbar
static let flightCancelled = Color(hex: "#EF4444") // Vermelho
static let flightBoarding = Color(hex: "#3B82F6") // Azul
// Cores de UI
static let flightPrimary = Color(hex: "#1F2937")
static let flightSecondary = Color(hex: "#6B7280")
static let flightBackground = Color(hex: "#F9FAFB")
static let flightCard = Color(hex: "#FFFFFF")
// Destaque
static let flightAccent = Color(hex: "#6366F1") // Índigo
}
Tipografia
struct FlightyTypography {
// Números de voo e portões (precisam se destacar)
static let flightNumber = Font.system(size: 20, weight: .bold, design: .monospaced)
static let gate = Font.system(size: 24, weight: .bold, design: .rounded)
// Exibições de tempo
static let time = Font.system(size: 18, weight: .medium, design: .monospaced)
static let countdown = Font.system(size: 32, weight: .bold, design: .monospaced)
// Status e rótulos
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)
}
Filosofia de Animação
Movimento com Propósito
As animações do Flighty servem à função, não à decoração.
// Animação de mudança de status
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()) // Mudanças de números suaves
.animation(.spring(response: 0.3), value: status)
}
}
// Animação da barra de progresso
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) // Lenta, contínua
}
}
}
Lições para Nosso Trabalho
1. Analogias do Mundo Real Guiam o Design
A sinalização aeroportuária tem mais de 50 anos de otimização. Aprenda com sistemas comprovados.
2. Estados Baseados em Contexto Reduzem a Carga Cognitiva
15 estados inteligentes significam que os usuários nunca veem informações irrelevantes.
3. Obviamente Simples É o Objetivo
Se o app “simplesmente funciona”, você teve sucesso. Atrito é fracasso.
4. Empacote, Organize, Colora Seus Dados
Não mostre dados brutos. Apresente insights. Codifique por cores o status.
5. Offline-First para Mobile
Voos ficam offline. O app deve funcionar sem conexão.
Perguntas Frequentes
Como funcionam os 15 estados inteligentes do Flighty?
O Flighty monitora o status do seu voo, localização e horário para mostrar automaticamente as informações mais relevantes. Os estados vão desde “muito antecipado” (24+ horas antes) passando por “ir para o aeroporto”, “no portão”, “embarcando”, “em voo” e “pousado”. Cada estado apresenta detalhes diferentes: números de confirmação no início, informações do portão quando relevantes, tempo de voo restante enquanto no ar, e orientações de conexão após pousar.
O que torna a implementação de Live Activities do Flighty especial?
O Flighty foi o primeiro app a aproveitar totalmente as Live Activities e Dynamic Island do iOS 16. A implementação inclui visualizações compactas mostrando portão e contagem regressiva, visualizações expandidas com detalhes completos do voo, e uma visualização de progresso em voo que funciona offline sem conectividade. O design trata as Live Activities como uma interface principal, não como um complemento.
Como o Flighty lida com cenários offline?
O Flighty baixa dados do voo e os armazena localmente antes da partida. O progresso em voo continua baseado no horário programado e no desempenho conhecido da aeronave. O app pré-calcula estimativas de chegada, tempos de conexão e informações do portão. Quando a conectividade retorna, os dados são sincronizados e atualizados com quaisquer mudanças. Essa abordagem offline-first é essencial já que os voos passam horas sem internet.
Por que o Flighty usa sinalização aeroportuária como inspiração de design?
Os painéis de embarque dos aeroportos foram refinados por mais de 50 anos para comunicar informações de voo rapidamente para viajantes estressados em ambientes movimentados. Eles usam uma linha por voo, status codificado por cores e apenas informações essenciais. O Flighty adapta essa linguagem visual comprovada para dispositivos móveis, usando os mesmos princípios de densidade, clareza e indicação de status que os aeroportos otimizaram por décadas.
Como o assistente de conexão calcula conexões apertadas?
O assistente de conexão compara seu horário de chegada com a próxima partida, então calcula o tempo de caminhada baseado na distância do portão dentro do terminal. Ele considera velocidades típicas de caminhada e layouts do terminal. Se a margem cair abaixo de 15 minutos, o app sinaliza como uma conexão apertada e mostra um mapa ao vivo com sua localização atual, portão de destino e rota de caminhada.