6 miniosswiftswiftui

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:

  1. Um ActivityAttributes com dados estáticos da atividade (não mudam durante a vida dela)
  2. Um ContentState com dados dinâmicos (atualizam ao longo do tempo)
  3. Uma view SwiftUI para a tela de bloqueio
  4. 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.