Live Activity e Dynamic Island na prática: explorando essas APIs do iOS
Notas técnicas sobre as APIs de Live Activity e Dynamic Island no iOS — quando usar, limites, e o que você precisa saber antes de adicionar a um app SwiftUI.
Quando a Apple introduziu Live Activities no iOS 16.1 e a Dynamic Island chegou no iPhone 14 Pro, virou uma das APIs mais fáceis de admirar e mais difíceis de usar bem. Fácil porque o efeito visual é incrível — informação relevante na tela de bloqueio, atualizando em tempo real, sem o app aberto. Difícil porque os limites são apertados: você só pode usar em casos específicos, com payload de tamanho limitado, e atualização sob restrições.
Esse post é sobre o que aprendi explorando essas APIs em um projeto pessoal em desenvolvimento. Não vou entrar em código de produto — é mais um diário técnico do que um tutorial.
O que é uma Live Activity
Tecnicamente, Live Activity é uma extensão Widget executando em um lifecycle especial. Você define:
- Um ActivityAttributes com dados estáticos da atividade (não mudam durante a vida dela)
- Um ContentState com dados dinâmicos (atualizam ao longo do tempo)
- Uma view SwiftUI para a tela de bloqueio
- Uma view SwiftUI para a Dynamic Island (em três tamanhos: compact, expanded e minimal)
Estrutura mínima:
import ActivityKit
import SwiftUI
import WidgetKit
struct ExampleAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var status: String
var progress: Double
}
var title: String
}
E o widget:
struct ExampleLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: ExampleAttributes.self) { context in
// View da tela de bloqueio
VStack {
Text(context.attributes.title)
Text(context.state.status)
ProgressView(value: context.state.progress)
}
.activityBackgroundTint(.indigo)
} dynamicIsland: { context in
DynamicIsland {
// Expanded
DynamicIslandExpandedRegion(.center) {
Text(context.state.status)
}
} compactLeading: {
Image(systemName: "circle.fill")
} compactTrailing: {
Text("\(Int(context.state.progress * 100))%")
} minimal: {
Image(systemName: "circle.fill")
}
}
}
}
Bonito. Mas tem armadilhas.
Limite 1: o caso de uso
Apple é explícita sobre quando Live Activity é apropriada:
"Use Live Activities to display up-to-date information from your app, allowing people to see the current state of an ongoing event or task."
A palavra-chave é ongoing. Live Activity não é pra "fixar um lembrete" ou "mostrar uma notificação persistente". É pra eventos com começo, meio e fim que o usuário quer acompanhar:
- Pedido de delivery em andamento
- Corrida do Uber
- Partida esportiva ao vivo
- Cronômetro / timer ativo
- Treino em curso
Apple revisa rejeições baseadas nisso. "Atividade" precisa fazer sentido como evento contínuo.
Limite 2: tamanho do payload
O ContentState é serializado. Tem um limite de 4KB. Isso parece muito, mas se você tentar enfiar uma lista de items, fotos como base64 ou strings longas, estoura rápido.
Estratégia: o ContentState carrega só o estado que muda. Detalhes pesados ficam nos attributes (estáticos) ou são carregados do app principal quando o usuário abre.
// Bom
struct ContentState: Codable, Hashable {
var status: ActivityStatus // enum com 5 casos
var lastUpdate: Date
var progress: Double
}
// Ruim
struct ContentState: Codable, Hashable {
var fullHistory: [HistoryEvent] // pode estourar 4KB
var imageData: Data // imagem em base64? não
var detailedDescription: String // longo? cuidado
}
Limite 3: como você atualiza
Tem três jeitos de atualizar uma Live Activity:
1. Do app aberto, com update:
let activity = Activity<ExampleAttributes>.activities.first
await activity?.update(ActivityContent(
state: newState,
staleDate: Date().addingTimeInterval(60)
))
2. Via push notification (APNs com tipo liveactivity):
Aqui complica. Você precisa de servidor, certificado APNs, e o token específico da Live Activity (diferente do device token). É a única forma de atualizar quando o app está completamente fechado.
3. Não atualizar — deixar a Live Activity expirar:
Se a atividade tem fim previsível, você pode simplesmente esperar a staleDate chegar. iOS marca como "stale" e eventualmente remove.
Limite 4: Dynamic Island tem 3 tamanhos
A Dynamic Island não é uma view só. São três views:
- Compact — quando há uma única atividade, ao redor da Dynamic Island fechada (leading + trailing)
- Expanded — quando o usuário toca e segura, ou quando há uma atividade prioritária
- Minimal — quando há múltiplas atividades; só uma se vê
Você precisa desenhar as três. E "compact leading + trailing" tem largura ridiculamente pequena — algo como 40px de cada lado. Cabem ícones e números curtos. Texto longo é cortado.
Esse limite é o mais subestimado quando se começa. Você desenha algo bonito na expanded view e descobre que a compact não cabe.
Limite 5: lifecycle e StaleDate
Toda Live Activity tem uma staleDate. Depois dela, iOS marca como "informação possivelmente desatualizada" e diminui a prioridade.
Se você não definir staleDate, iOS aplica seu próprio (geralmente curto). Para uma atividade de 8 horas (turno de trabalho, corrida longa), você precisa definir explicitamente:
let staleDate = Date().addingTimeInterval(8 * 3600)
let request = ActivityContent(state: state, staleDate: staleDate)
Mesmo assim, iOS pode encerrar sua atividade a qualquer momento. Se o usuário usa o app pouco, se há pressão de memória, se o usuário tem 8 outras atividades ativas — qualquer uma dessas razões. Sua activity precisa funcionar no app principal mesmo se a Live Activity desaparecer.
O que fica
Live Activities valem a pena quando:
- O caso de uso é ongoing (não notificação)
- O estado é pequeno e mudável
- Você desenha pra três tamanhos de Dynamic Island
- Você aceita que iOS pode encerrar sua atividade a qualquer momento
Quando essas condições batem, é uma das APIs mais gratificantes pra trabalhar. O efeito visual de ver sua informação no Dynamic Island, atualizando, sem o app aberto — é o tipo de detalhe que faz o usuário se apegar ao produto.
Mas é um detalhe. Live Activity é tempero. Se a app não funciona bem sem Live Activity, dificilmente vai funcionar bem com ela.
Esse foi o aprendizado que mais carreguei: adicione Live Activity depois que o produto principal está sólido. Antes disso, é puxar a complexidade pra cedo demais.