← Todos os Posts

Live Activities são uma máquina de estados, não um badge

A Live Activity no Return parece um número de contagem regressiva na Lock Screen e na Dynamic Island.12 Não é um número. É uma máquina com cinco estados de ciclo de vida, três caminhos externos de dispensa e um caminho de início reentrante que precisa se defender de si mesmo. Os padrões abaixo são os que sobreviveram em produção. O rodapé de honestidade brutal no fim diz o que ainda não sei.

Lancei uma v1 que tratava a Live Activity como um badge. O “tempo restante atual” era dado; o resto era decoração. Aquela versão tinha três bugs que peguei no TestFlight e um que peguei em produção:

  1. Tocar em iniciar enquanto o início já estava em andamento criava uma segunda atividade que orfanava a primeira.
  2. A contagem regressiva renderizava corretamente na Dynamic Island, mas a view da Lock Screen batia em endTime <= Date() para timers pausados e mostrava 0:00 até o usuário retomar.
  3. A Live Activity continuava visível muito tempo depois de o usuário resetar o timer porque a política de dispensa era .default, que a Apple mantém visível por algum tempo, até quatro horas.
  4. (Produção.) Em locales de idiomas Right-to-Left (árabe, hebraico), os dígitos renderizavam ao contrário na região compact-trailing da Dynamic Island. Dígitos latinos, layout RTL. A correção foi uma linha.

Cada um desses era um bug de máquina de estados. O número da contagem regressiva estava bem. O número não é o produto. O produto é o estado.

A máquina de estados abaixo é o que sobreviveu a esses bugs.

TL;DR

  • O LiveActivityManager em produção expõe 5 métodos de transição (startActivity, updateActivity, showCycleComplete, showFinalCompletion, endActivity) mais 1 leitura (hasActiveActivity). As 224 linhas de produção protegem contra um risco específico dentro de startActivity: chamadas concorrentes de start mais checagens de cancelamento em cada fronteira await desse método.3
  • O ContentState carrega 6 campos: endTime, currentCycle, totalCycles, isPaused, isCompleted, remainingSeconds. Os primeiros cinco são os rótulos da máquina de estados. O sexto (remainingSeconds) é um fallback de exibição estática que o timerInterval ao vivo do ActivityKit não consegue servir.
  • A decisão da política de dispensa é a real chamada de produto. .immediate para reset do usuário, .after(Date().addingTimeInterval(3)) para conclusão, nunca o default do sistema.
  • A região compact-trailing da Dynamic Island precisa de .environment(\.layoutDirection, .leftToRight) no texto do timer para manter os dígitos latinos em LTR sob locales de sistema RTL.

A máquina de estados

A Live Activity em produção tem um estado idle, três estados ao vivo que o usuário pode observar, um estado terminal e um portão reentrante que o desenvolvedor precisa observar:

┌──────────────────────────────────────────────────────────────────┐
│                  Lifecycle states                                 │
├──────────────────────────────────────────────────────────────────┤
│  IDLE          currentActivity == nil; no Live Activity present   │
│  RUNNING       isPaused=false, endTime > Date()                   │
│  PAUSED        isPaused=true, remainingSeconds=N                  │
│  CYCLE_END     isPaused=false, endTime <= Date(), isCompleted=false│
│  COMPLETE      isCompleted=true (terminal; transitions to IDLE)   │
└──────────────────────────────────────────────────────────────────┘
              │
              ↓
┌──────────────────────────────────────────────────────────────────┐
│             Dismissal policies (Apple)                            │
├──────────────────────────────────────────────────────────────────┤
│  .immediate            user reset                                  │
│  .after(now + 3s)      completion display window                   │
│  .default              system decides; can stay up to 4 hours      │
└──────────────────────────────────────────────────────────────────┘

Reentrancy gate inside startActivity():
  isStartingActivity flag + cancellable startActivityTask
  prevents two concurrent startActivity() calls from creating
  two Live Activities for one timer. Cancellation checks across
  each await keep the in-flight task safe to abort.

O caminho de renderização verifica isPaused primeiro; essa ordenação é o que impede um timer pausado de renderizar como CYCLE_END quando o relógio de parede cruzou endTime.7

Os nomes dos estados não são rótulos sobre o número. Os nomes dos estados são o contrato entre LiveActivityManager (o lado do app, onde minhas views SwiftUI vivem) e ReturnLiveActivity (a extensão de widget, onde o processo da Apple renderiza a superfície).

O contrato é TimerActivityAttributes.ContentState, todos os 6 campos:3

public struct ContentState: Codable, Hashable {
    var endTime: Date
    var currentCycle: Int
    var totalCycles: Int?
    var isPaused: Bool
    var isCompleted: Bool = false
    var remainingSeconds: Int = 0
}

Cada transição de estado muta essa struct e pede ao ActivityKit para entregá-la através das fronteiras de processo até a extensão de widget. O widget então renderiza novamente. Não há memória compartilhada. Não há callback. Há uma struct Codable que cruza uma fronteira de processo a cada transição.

Esse fato exclui qualquer coisa que eu possa querer fazer com closures, view models, observable objects ou propriedades computadas. O estado precisa ser expressável como dados serializáveis. Se não puder ser codificado, não pode transitar.

O início reentrante

Live Activities têm um limite rígido sobre atividades concorrentes e um limite suave sobre o que acontece se você chamar Activity.request duas vezes em andamento. O limite rígido está bem documentado.4 O limite suave é “a segunda chamada pode ter sucesso e criar um órfão.” O órfão é a Live Activity que não está mais associada a currentActivity no seu manager. Ela sobrevive. Não tem caminho de volta para o seu código. Eventualmente se dispensa pelo seu próprio timer de obsolescência. O usuário vê um timer duplicado até lá.

O órfão foi o bug v1 que o Return enviou. A correção é o portão reentrante mais um Task cancelável em LiveActivityManager.swift:3

private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?

func startActivity(...) {
    #if os(iOS)
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
    guard !isStartingActivity else { return }
    isStartingActivity = true

    startActivityTask?.cancel()

    startActivityTask = Task {
        defer {
            isStartingActivity = false
            startActivityTask = nil
        }
        guard !Task.isCancelled else { return }

        await endActivity()  // explicit cleanup of any prior state

        guard !Task.isCancelled else { return }

        // ... build attributes + contentState ...

        do {
            let activity = try Activity.request(...)
            guard !Task.isCancelled else { return }
            currentActivity = activity
        } catch {
            // log; flag clears via defer
        }
    }
    #endif
}

Três coisas sobre esse padrão que a documentação não menciona:

A flag isStartingActivity é a proteção ativa; startActivityTask?.cancel() é uma limpeza defensiva. A flag faz curto-circuito em qualquer segunda chamada de startActivity enquanto a primeira está em andamento, então você não corre uma race no caminho público de fato. A dança de cancelar-e-substituir ainda importa porque o Task em andamento é assíncrono e pode sobreviver a um chamador de vida curta; o cancelamento impede que um Task obsoleto continue depois que o chamador seguiu adiante.

As checagens guard !Task.isCancelled em cada fronteira await. O cancelamento é cooperativo em Swift. Mesmo que cancel seja chamado, o Task continua rodando até verificar explicitamente. Cada await é uma oportunidade de verificar. Sem as checagens pós-await, um Task cancelado continua construindo o estado da atividade, chama Activity.request e silenciosamente cria um órfão em caso de sucesso.

O defer limpa a flag antes do corpo do Task completar. Sem defer, um return antecipado (da checagem de cancelamento) deixa isStartingActivity = true permanentemente e a atividade nunca mais inicia até o relançamento do app. A flag é um lock; o lock precisa liberar em cada caminho de saída.

O argumento pushType: nil. O Return não usa atualizações de Live Activity enviadas via APNs. O app atualiza a atividade localmente via activity.update. Se você precisa de atualizações por push (rastreamento de entrega, placares esportivos, dados em tempo real), o tipo é pushType: .token e o contrato é dramaticamente mais complexo.5 Atualizações locais são mais simples e cobrem qualquer fluxo de timer / contador / app único.

O problema da pausa

O ActivityKit traz uma view linda Text(timerInterval: Date()...endTime, countsDown: true) que renderiza uma contagem regressiva ao vivo sem qualquer atualização do app.6 Você define o tempo final, o sistema renderiza um timer ao vivo. Sem Timer.publish, sem refresh do widget, sem dreno de bateria.

Isso é fantástico quando o timer está rodando. É errado quando o timer está pausado.

O texto timerInterval conta em direção a endTime independentemente de qualquer sinal de “pausa” no estado. Não há modo “congelado em 10:23” no API da Apple. Se você passar endTime = Date().addingTimeInterval(623) e o usuário pausar na marca de 10:23, o texto do timer continua contando regressivamente até zero no widget. O campo de estado diz pausado. O widget renderiza rodando.

A correção é renderizar duas views diferentes a partir do mesmo estado:7

if context.state.isPaused {
    // static text
    Text(formatTime(context.state.remainingSeconds))
        .monospacedDigit()
} else if context.state.endTime > Date() {
    // live countdown
    Text(timerInterval: Date()...context.state.endTime, countsDown: true)
        .monospacedDigit()
} else {
    // post-end static
    Text("0:00")
        .monospacedDigit()
}

A renderização em duas trilhas é o motivo pelo qual o ContentState carrega remainingSeconds como um campo separado. É redundante quando o timer está rodando (o sistema computa a partir de endTime). É a única fonte de verdade quando o timer está pausado. As duas metades da struct servem a dois modos diferentes de renderização; o booleano isPaused seleciona entre eles.

As políticas de dispensa

activity.end(_:dismissalPolicy:) aceita um de três valores ActivityUIDismissalPolicy, e escolher errado foi o que fez minha v1 ficar na Lock Screen do usuário pelo que pareceu uma eternidade depois de um reset:13

Política Quando usar O que você obtém
.immediate Reset do usuário, erro, app em background sem atividade para rastrear A atividade desaparece agora. Sem janela de tolerância
.after(date) Exibição de conclusão: “sua meditação está completa” precisa ficar legível por um momento. A data deve estar dentro da janela de quatro horas que a Apple permite A atividade mostra o estado final, então é dispensada em date
.default Quando você genuinamente quer que a heurística da Apple decida O sistema mantém visível “por algum tempo” (palavras da Apple), até quatro horas depois de end ser chamado

O Return usa .after(Date().addingTimeInterval(3)) para o caminho natural de conclusão:3

await activity.end(
    .init(state: contentState, staleDate: nil),
    dismissalPolicy: .after(Date().addingTimeInterval(3))
)

Três segundos é o tempo que um usuário precisa para olhar a Lock Screen, registrar que o timer terminou e sentir a satisfação do checkmark. Menos de três é nervoso. Mais de três parece que a atividade não sabe que terminou.

Para um reset disparado pelo usuário, a chamada é dismissalPolicy: .immediate. Sem janela. O usuário já sabe.

A escolha errada na v1 foi .default. Para um timer de meditação concluído, o sistema mantinha a atividade visível por tempo suficiente para os usuários acharem que o app não tinha registrado a conclusão. A documentação da Apple diz que .default mantém a atividade encerrada “visível por algum tempo” até quatro horas;13 a postura correta para um timer é tornar a dispensa explícita.

A região compact da Dynamic Island

A Dynamic Island tem três modos de renderização e você precisa dos três até para um timer simples:2

  • Compact (formato padrão da Dynamic Island): ícone leading + timer trailing
  • Minimal (quando outra Live Activity compete pela mesma Dynamic Island): apenas ícone leading
  • Expanded (long-press): quatro regiões nomeadas (leading, trailing, center, bottom)

O padrão que ganhou seu lugar no Return é tornar a view expandida quase idêntica à compact:8

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image("AppIconSmall")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 16, height: 16)
            .clipShape(RoundedRectangle(cornerRadius: 4))
    }
    DynamicIslandExpandedRegion(.trailing) {
        TimerText(...)
    }
    DynamicIslandExpandedRegion(.center) { EmptyView() }
    DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
    Image("AppIconSmall")...
} compactTrailing: {
    TimerText(...)
} minimal: {
    Image("AppIconSmall")...
}

A maioria dos tutoriais de Live Activity se apoia na view expandida como o “design real”, com conteúdo rico na região bottom. Para um timer de meditação, a expansão é peso morto. O usuário abre a view expandida com long-press, e o long-press já dá o feedback háptico de que algo aconteceu. Adicionar conteúdo faz a expansão dizer algo que o usuário não pediu. Regiões vazias no modo expandido não são uma falha de design; são o design.

O bug RTL

O bug de produção. Usuários de árabe e hebraico no iOS reportaram que o timer compact-trailing da Dynamic Island renderizava os dígitos ao contrário. A string numérica latina 5:23 estava renderizando como 32:5 porque a direção de layout do compact-trailing herdava a configuração RTL do locale do sistema.

SwiftUI herda a direção de layout do sistema dentro do processo do widget, então o texto do timer da Dynamic Island pegava RTL quando o telefone do usuário estava configurado em árabe ou hebraico. Numerais latinos deveriam renderizar em LTR mesmo dentro de uma UI RTL no resto. A correção é fixar a direção de layout nas views de texto numérico:7

.environment(\.layoutDirection, .leftToRight)

O override vai nas views numéricas Text dentro de TimerText (compact / expanded da Dynamic Island) e dentro da view da Lock Screen, não na view inteira. Dígitos latinos leem da esquerda para a direita independentemente do locale do sistema do usuário; rótulos de ciclo como “Ciclo 2 de 3” permanecem localizados para seguir a direção de layout do sistema.

O bug não aparece em TestFlight de locale doméstico. Aparece no momento em que um usuário RTL real abre o timer. A lição: enviar o override de ambiente fixado em LTR em cada view de texto com dígitos latinos em qualquer Live Activity que possa rodar em locales RTL.

A história de localização

TimerActivityAttributes carrega um campo languageCode: String definido pelo app na criação da atividade:9

let attributes = TimerActivityAttributes(
    timerDuration: duration,
    languageCode: settings.appLanguage  // app's selected language, not system's
)

A extensão de widget lê isso para renderizar strings localizadas:

private var locale: Locale {
    let code = context.attributes.languageCode
    return code.isEmpty ? .current : Locale(identifier: code)
}

private func localized(_ key: String.LocalizationValue) -> String {
    String(localized: key, locale: locale)
}

Por que o app passa seu próprio código de idioma em vez de deixar o widget ler Locale.current: a extensão de widget roda em seu próprio processo. Seu Locale.current é o locale do sistema, não o locale selecionado do app. Se um usuário definiu o Return como coreano enquanto o iPhone está em inglês, o widget falaria inglês sem este override. A preferência de idioma do app viaja nos atributos da atividade; o widget a respeita.

Localizable.xcstrings vive no target do widget junto com o do app, mas são arquivos separados. Strings usadas no widget precisam existir em ReturnWidgets/Localizable.xcstrings mesmo se a mesma string existir em Return/Localizable.xcstrings. Esquecer disso significa que o widget cai de volta para o idioma de desenvolvimento enquanto o app fala coreano.

O que eu construiria diferente

Tornar o ContentState menor. Seis campos é demais. A redundância entre endTime e remainingSeconds é o preço de contornar a ausência de modo-pausa em timerInterval. Se eu estivesse começando do zero, carregaria um único enum displayMode (running, paused(remainingSeconds: Int), cycleEnd, complete) e deixaria o código de renderização despachar pelo case. Seis campos são mais difíceis de manter corretamente mutados em cinco métodos de transição do que quatro cases são.

Adicionar botões interativos de Live Activity (iOS 17+). O Return atualmente não expõe controles de pause/resume na Dynamic Island. O usuário precisa abrir o app para pausar. iOS 17 adicionou Button(intent:) para App Intents dentro de Live Activities.10 Um controle interativo de pausa é a extensão óbvia e a próxima coisa que vou enviar para o Return.

Live Activities com push-update para sincronização de timer entre dispositivos. O Return sincroniza sessões entre iPhone, iPad, Watch e Apple TV via NSUbiquitousKeyValueStore (coberto em Five Apple Platforms, Three Shared Files). Hoje a atividade é iniciada localmente do app do iPhone ou iPad e atualizada localmente. Um usuário iniciando um timer no Apple Watch poderia idealmente ver a Live Activity refletir isso no iPhone em tempo real. Push APNs para a Live Activity é o caminho.5 Não construí ainda.

Quando não usar Live Activities

Estado transitório de uma única ocorrência. Um toast de “salvo!” não merece uma Live Activity. O sistema tem um banner. Use-o.

Dados que mudam com frequência sem uma dimensão temporal. Live Activities funcionam melhor para coisas com uma âncora temporal clara (um timer, um ETA de entrega, um relógio de jogo, a duração de uma chamada telefônica). Cotações de ações e placares esportivos funcionam porque têm uma janela de sessão. Um dashboard de propósito geral não tem.

Apps sem caso de uso de Lock Screen / standby. Live Activities exigem investimento real de engenharia (configuração de target, design de ContentState, decisões de política de dispensa, tratamento de RTL, encanamento de localização). Apps que o usuário abre diretamente sem nunca consultar a Lock Screen durante o uso não são o formato certo. Um editor de fotos não precisa de uma. Um rastreador de treino precisa.

Em superfícies não-iOS, com ressalvas. O LiveActivityManager do Return envia sua implementação atrás de #if os(iOS) porque o timer é iniciado pelo app do iPhone ou iPad. O próprio ActivityKit descreve banner da Lock Screen, Dynamic Island, Smart Stack do Apple Watch, Mac e CarPlay como superfícies de apresentação; iOS 26 expandiu várias delas.4 watchOS ainda tem suas próprias complications API para renderização em tela cheia. macOS tem apps de menu bar. iPadOS suporta Live Activities desde iPadOS 17 sem região de Dynamic Island. O manager do Return tem 8 guards #if os(iOS) em um único arquivo de 224 linhas.

O que o padrão significa para apps que enviam em iOS 26+

Duas conclusões.

  1. Trate a Live Activity como uma máquina de estados, não como um número. A máquina de estados tem estados claros, transições claras e regras de dispensa claras. O número na tela é uma renderização de um estado. Acerte os estados primeiro.

  2. O guard de reentrância é o bug que você ainda não pegou. Cada Live Activity manager que vi solto que não implementa isStartingActivity + Task cancelável enviou pelo menos um bug de atividade-órfã. O guard tem 6 linhas. Escreva-o uma vez.

Combine este post com meus escritos anteriores para a mesma família de apps: App Intents tipados para Apple Intelligence; servidores MCP para agentes cross-LLM; padrões Liquid Glass para a camada visual; envio multi-plataforma para alcance entre dispositivos. Live Activities são a camada iOS-Lock-Screen-e-Dynamic-Island da mesma stack. O conjunto completo vive no hub Apple Ecosystem Series. Para o contexto mais amplo de iOS-com-agentes-de-IA, veja o guia iOS Agent Development.

FAQ

Qual é a diferença entre Live Activities e widgets WidgetKit?

Widgets WidgetKit renderizam em intervalos definidos por TimelineProvider; o sistema decide quando atualizar e o widget renderiza novamente a partir de uma timeline estática.11 Live Activities renderizam em resposta a chamadas específicas de activity.update(...) dirigidas pelo app e vivem pela duração da atividade subjacente (um timer, uma entrega, um treino). Ambos são enviados no target da extensão de widget; a diferença é o modelo de gatilho.

Live Activities funcionam no iPad?

Sim, no iPadOS 17+. O banner da Lock Screen é a superfície primária de renderização; iPad não tem Dynamic Island. O mesmo código de ActivityConfiguration funciona; apenas espere que as regiões da Dynamic Island nunca renderizem no iPad.

Uma Live Activity pode sobreviver ao processo do meu app?

Sim. Uma vez que Activity.request tenha sucesso, o ActivityKit é dono da atividade. O processo do app pode ser terminado pelo sistema; a atividade continua renderizando na Lock Screen e Dynamic Island até você terminá-la explicitamente (ou até que as regras de obsolescência do sistema a dispensem). Chamadas explícitas de endActivity() importam por essa razão; sem um end explícito no reset do app, a atividade sobrevive ao timer.

Por que o post não cobre Live Activities atualizadas por push?

Não enviei Live Activities atualizadas por push no Return. Pela regra de gênero deste cluster: posts de código enviado documentam apenas o que o código de produção faz. Atualizações por push estão listadas em “O que eu construiria diferente”; um post futuro vai cobri-las depois que eu enviar.

Qual é o layout real de arquivos para Live Activities em um app SwiftUI?

Três peças:3712

  • No target principal do app: LiveActivityManager.swift (gerencia o ciclo de vida da atividade), TimerActivityAttributes.swift (a struct ActivityAttributes compartilhada com o widget; ambos os targets compilam este arquivo).
  • Em um target de extensão de widget: ReturnLiveActivity.swift (a conformidade Widget com o corpo ActivityConfiguration), ReturnWidgetsBundle.swift (o @main WidgetBundle).
  • Configuração: Info.plist com NSSupportsLiveActivities = YES no target do app.

O target da extensão de widget precisa de imports do ActivityKit e do WidgetKit. TimerActivityAttributes é o único arquivo compartilhado entre ambos os targets; todo o resto é isolado por target.


A Live Activity não é um número na Lock Screen. É uma máquina de estados que cruza uma fronteira de processo a cada transição. Acerte os estados, proteja a reentrância, escolha a política de dispensa de propósito e fixe a direção de layout. O número se cuida sozinho.

Referências


  1. Return do autor, um timer de meditação SwiftUI publicado na App Store em 21 de abril de 2026, disponível para iPhone, iPad, Mac, Apple Watch e Apple TV. Live Activities são enviadas apenas no target iOS. 

  2. Apple Developer, “ActivityKit framework”. Banner da Lock Screen, modos compact / minimal / expanded da Dynamic Island, ciclo de vida da atividade. Disponível iOS 16.1+; Dynamic Island disponível no iPhone 14 Pro e posterior. 

  3. Código de produção em Return/Return/LiveActivityManager.swift (224 linhas, 8 blocos #if os(iOS)) e Return/Return/TimerActivityAttributes.swift (43 linhas). Compartilhado entre o target do app e o target da extensão de widget via membership de target. 

  4. Apple Developer, “Displaying live data with Live Activities”. Limites de concorrência, plataformas suportadas (iOS 16.1+, iPadOS 17+), chave NSSupportsLiveActivities do Info.plist. 

  5. Apple Developer, “Updating and ending your Live Activity with ActivityKit push notifications”. O caminho pushType: .token requer uma chave separada de auth APNs, registro de push token no servidor e um protocolo de atualização diferente das chamadas locais activity.update(...)

  6. Apple Developer, “Text(timerInterval:pauseTime:countsDown:showsHours:)”. Timer de contagem regressiva ao vivo renderizado pelo sistema; renderiza sem atualizações do app enquanto a atividade está rodando. 

  7. Código de produção em Return/ReturnWidgets/ReturnLiveActivity.swift (232 linhas). A conformidade Widget da extensão de widget com o corpo ActivityConfiguration<TimerActivityAttributes>. A view TimerText nas linhas 61-102 lida com a renderização de três estados: pausado / rodando / pós-fim. 

  8. Apple Developer, “DynamicIsland”. As quatro regiões expandidas nomeadas (leading, trailing, center, bottom) mais três views de modo compact (compactLeading, compactTrailing, minimal). 

  9. A extensão de widget roda em seu próprio processo e herda o locale do sistema, não o locale selecionado do app. Apps que suportam troca de idioma in-app (Return suporta 27 idiomas) precisam passar o código de idioma por ActivityAttributes para que o widget possa renderizar no idioma escolhido pelo usuário. Padrão: Locale(identifier: context.attributes.languageCode) em vez de Locale.current

  10. Apple Developer, “Button(intent:)”. Disponível em widgets e views de Live Activity a partir de iOS 17+. Faz a ponte de App Intents para controles de Lock Screen / Dynamic Island sem exigir que o app vá para foreground. 

  11. Apple Developer, “TimelineProvider”. O modelo de refresh de widget anterior às Live Activities; entradas pré-computadas com janelas de reload gerenciadas pelo sistema. 

  12. Código de produção em Return/ReturnWidgets/ReturnWidgetsBundle.swift (16 linhas). O @main WidgetBundle que registra ReturnLiveActivity como o único widget da extensão de widget. Padrão obrigatório para extensões de widget; o bundle é o que o sistema carrega. 

  13. Apple Developer, “ActivityUIDismissalPolicy”. Três cases: .default, .immediate, .after(_:). A Apple afirma que .default mantém uma Live Activity encerrada visível “por algum tempo” até quatro horas, e .after(_:) aceita uma data dentro da mesma janela de quatro horas. 

Artigos relacionados

A superfície de widgets do iOS 26: um App Intent, muitos lugares

Os widgets do iOS 26, os controles da Central de Controle e as Live Activities são todos superfícies de App Intents. Um …

10 min de leitura

Liquid Glass no SwiftUI: três padrões de quem lançou o Return no iOS 26

O Liquid Glass da Apple é uma API SwiftUI de uma linha. Três padrões do Return vão além do .glassEffect(): glass sobre t…

18 min de leitura

Loop Engineering: Loops Win Where Verification Is Cheap

Loop engineering, checked against Boris Cherny's full transcripts: every loop he names has cheap verification. That cons…

19 min de leitura