← Todos os Posts

Ciclo de vida de treinos no HealthKit: estados de HKWorkoutSession e a superfície multiplataforma do iOS 26

HKWorkoutSession é a máquina de estados de treinos do HealthKit. A sessão passa por seis estados (.notStarted, .prepared, .running, .paused, .stopped, .ended), expõe eventos de ciclo de vida através de HKWorkoutSessionDelegate e (desde o iOS 26) roda no iPhone além do Apple Watch1. Um HKLiveWorkoutBuilder pareado com a sessão coleta amostras e eventos de forma incremental; o iOS 26 trouxe o mesmo builder API para o iPhone. O post do cluster sobre o Contrato de runtime do watchOS defendeu que apps watchOS precisam de um tipo de sessão reconhecido para continuar rodando em segundo plano; HKWorkoutSession é um desses tipos de sessão, e os estados do ciclo de vida mapeiam diretamente para o modelo de runtime.

O post percorre o ciclo de vida do treino contra a documentação da Apple. O enquadramento é “o que cada estado permite e o que cada transição dispara”, porque apps de treino que gerenciam mal o ciclo de vida ou perdem dados (transição para fora do running cedo demais) ou esgotam a bateria (não fazem a transição para fora do running de jeito nenhum).

TL;DR

  • Ciclo de vida do HKWorkoutSession: notStartedpreparedrunning → (opcional pausedrunning) → stoppedended. As transições são reportadas através de HKWorkoutSessionDelegate.workoutSession(_:didChangeTo:from:date:)2.
  • HKLiveWorkoutBuilder é o acumulador de dados ao vivo pareado com a sessão de treino. Surgiu no watchOS em 2018 e chegou ao iOS 26+, iPadOS 26+ e Mac Catalyst 26+. Treinos no iPhone usam o mesmo HKLiveWorkoutBuilder API que o Apple Watch, com diferenças específicas de plataforma no modelo de runtime e na disponibilidade de sensores3.
  • O método prepare() na sessão aquece os sensores antes que startActivity(_:) execute o treino propriamente dito. A recomendação da Apple é uma UI de contagem regressiva de 3 segundos entre prepare() e startActivity(_:) para dar tempo aos sensores de frequência cardíaca e dispositivos Bluetooth externos de se conectarem.
  • O estado stopped é transitório: apps podem finalizar métricas neste estado mas não podem retomar a sessão. Chamar end() faz a transição para ended, que é terminal.
  • O post do cluster sobre o Contrato de runtime do watchOS cobre como a sessão de treino mantém o app watchOS rodando ao baixar o pulso. A máquina de estados do ciclo de vida e o contrato de manter-vivo do runtime são as duas metades da mesma superfície.

Os seis estados

HKWorkoutSessionState enumera o ciclo de vida2:

.notStarted. A sessão foi criada mas não preparada. Os sensores não estão aquecidos; o app ainda não é considerado um host de treino ativo. A transição para .prepared acontece quando o app chama prepare().

.prepared. A sessão chamou prepare(); os sensores estão aquecendo mas o treino não começou. Monitores de frequência cardíaca conectam, sensores de movimento inicializam, o GPS obtém um sinal. O padrão voltado para o usuário é uma contagem regressiva de 3 segundos (“Prepare-se… 3, 2, 1, JÁ!”); durante essa janela, o sistema tem tempo de adquirir um sinal limpo para que as primeiras métricas no estado running sejam precisas.

.running. O estado de treino ativo. O app está coletando métricas, exibindo dados ao vivo e (no watchOS) mantendo a tela ligada através do contrato de runtime de treino-ativo. A transição para .running acontece através de startActivity(_:).

.paused. Um estado pausado pelo usuário. O app não está mais coletando métricas ativas (por exemplo, distância) mas a sessão é preservada; chamar resume() retorna para .running. O ciclo pausar/retomar pode acontecer qualquer número de vezes dentro de uma única sessão.

.stopped. Um estado pós-treino transitório. A sessão encerrou sua fase ativa mas não foi finalizada; o builder ao vivo ainda pode finalizar métricas. A partir de .stopped, chamar end() faz a transição para .ended. O app não pode retomar a partir de .stopped.

.ended. O estado terminal. A sessão acabou; o builder ao vivo foi instruído a finalizar; o treino é salvo no HealthKit (se o app chamou finishWorkout(completion:) no builder). Uma vez em .ended, a sessão não é mais manipulável.

O diagrama de estados tem uma pegadinha específica: não há caminho de .stopped de volta para .running. Um treino que o usuário quer “des-encerrar” precisa iniciar uma nova sessão, não retomar a antiga.

Os métodos que conduzem as transições

HKWorkoutSession expõe os seguintes métodos para transições de estado1:

  • prepare(). Faz a transição de .notStarted para .prepared. Aquece os sensores.
  • startActivity(with: Date). Faz a transição de .prepared para .running. O parâmetro Date permite que o app defina o horário oficial de início (tipicamente .now).
  • pause(). Faz a transição de .running para .paused.
  • resume(). Faz a transição de .paused de volta para .running.
  • stopActivity(with: Date). Faz a transição de .running (ou .paused) para .stopped. O Date é o horário oficial de término.
  • end(). Faz a transição de .stopped para .ended.

O padrão de prepare() para startActivity(_:) é a janela de aquecimento. O padrão de stopActivity(_:) para end() é a janela de limpeza: o builder ao vivo tem a chance de adicionar amostras finais antes que a sessão termine.

HKLiveWorkoutBuilder no watchOS e iPhone

HKLiveWorkoutBuilder é o acumulador de dados ao vivo pareado com a sessão3. O builder surgiu no watchOS no watchOS 5 e foi estendido para iOS 26+, iPadOS 26+ e Mac Catalyst 26+. O ciclo de vida do builder se pareia com o da sessão:

let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor

let session = try HKWorkoutSession(healthStore: store, configuration: configuration)
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)

session.delegate = self
builder.delegate = self

session.prepare()
// User taps Start after countdown
session.startActivity(with: Date())
try await builder.beginCollection(at: Date())

// During the workout, metrics flow into the builder via the data source.
// builder.collectedTypes contains the sample types being collected.
// builder.statistics(for:) returns running stats.

// User ends the workout
session.stopActivity(with: Date())
try await builder.endCollection(at: Date())
let workout = try await builder.finishWorkout()
session.end()

Três peças unem o treino:

  • HKWorkoutConfiguration especifica o tipo de atividade e a localização. O tipo de atividade conduz a seleção de métricas (um treino de corrida coleta o ritmo, um treino de ciclismo indoor não).
  • HKLiveWorkoutDataSource é a ponte da configuração de sensores da sessão para a acumulação de dados do builder. A fonte de dados publica amostras; o builder recebe e armazena.
  • HKLiveWorkoutBuilder mantém o estado do treino em andamento e finaliza o objeto HKWorkout salvo.

O padrão é incremental: amostras fluem continuamente durante o estado .running; o builder as integra em estatísticas em execução; o finishWorkout() final escreve o treino completo no HealthKit.

iOS 26: treinos no iPhone

O iOS 26 trouxe HKWorkoutSession para o iPhone com o mesmo HKLiveWorkoutBuilder e fonte de dados API que o watchOS usa4. A construção é a mesma; as diferenças específicas de plataforma estão no modelo de runtime, na disponibilidade de sensores e no tratamento de privacidade, em vez de na superfície API.

Os casos de uso que o API de treinos do iOS 26 habilita: - Apps de treino com o telefone como companheiro (o iPhone retém dados do monitor de frequência cardíaca ao lado da sessão do Watch). - Apps de fitness apenas para iPhone para usuários sem Apple Watch, onde o iPhone rastreia a sessão através de sensores integrados e acessórios conectados. - Continuidade de sessão entre dispositivos: uma sessão do Apple Watch que faz handoff para o iPhone (o usuário tira o Watch mas quer que o iPhone continue rastreando) ou vice-versa.

As diferenças de plataforma que vale nomear: - Disponibilidade de sensores. O iPhone tem acelerômetro e GPS mas não tem sensor de frequência cardíaca integrado. Apps que precisam de frequência cardíaca em treinos no iOS pareiam com uma cinta cardíaca Bluetooth ou leem de um Apple Watch conectado através do HealthKit. - Modelo de runtime. O runtime de treino-ativo do Apple Watch garante acesso contínuo aos sensores ao baixar o pulso. O runtime do iPhone depende do ciclo de vida normal de primeiro/segundo plano do sistema mais a recuperação de crash via scene delegate (coberta na sessão 322 do WWDC 2025), que é uma forma de garantia diferente. - Comportamento de privacidade e tela bloqueada. Treinos do iPhone rodando enquanto o dispositivo está bloqueado exigem configuração explícita para continuar coletando amostras, já que a tela bloqueada é uma fronteira de privacidade mais forte do que baixar o pulso.

O protocolo delegate

HKWorkoutSessionDelegate reporta transições de estado e erros5:

extension WorkoutCoordinator: HKWorkoutSessionDelegate {
    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didChangeTo toState: HKWorkoutSessionState,
        from fromState: HKWorkoutSessionState,
        date: Date
    ) {
        switch toState {
        case .running:
            // workout is active
        case .paused:
            // user paused
        case .stopped:
            // finalize metrics
        case .ended:
            // workout done; cleanup
        default:
            break
        }
    }

    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didFailWithError error: Error
    ) {
        // session failed (e.g., heart rate sensor disconnected unexpectedly)
    }
}

O delegate é a única fonte da verdade para transições de estado. Apps que inferem estado a partir de chamadas de método (chamei startActivity(), então estamos rodando agora) perdem mudanças de estado que o sistema aplica (auto-pausa quando o usuário está parado, auto-encerramento quando o relógio é removido). O padrão conduzido por delegate é o correto.

O contrato de runtime

HKWorkoutSession é um dos tipos de sessão que o watchOS reconhece para manter um app rodando em segundo plano, ao lado de sessões de mindfulness, alarme e gravação de áudio6. O contrato: enquanto a sessão está no estado .prepared, .running, .paused ou .stopped, o app continua rodando; a tela acende quando o usuário levanta o pulso; sensores transmitem para o app continuamente.

O post do cluster sobre o Contrato de runtime do watchOS cobre isso em detalhes. O ponto relevante para apps de treino: a máquina de estados do ciclo de vida é o que diz ao watchOS “mantenha este app rodando”; a transição para .ended libera o contrato e permite ao SO suspender o app.

Uma implicação prática: não encerre uma sessão de treino prematuramente. Se o usuário se afastar do treino para uma chamada telefônica e voltar, a sessão deve permanecer em .running (ou ser pausada via pause()), não ser encerrada. Encerrar e reiniciar perde os dados e a continuidade de runtime.

Falhas comuns

Três padrões dos logs de falhas de apps de treino:

Pular prepare(). Apps que chamam startActivity(_:) sem antes chamar prepare() produzem treinos em que os primeiros 5-10 segundos de dados de frequência cardíaca não são confiáveis (sensor não estava aquecido) ou estão ausentes (a cinta cardíaca Bluetooth não havia conectado). Correção: sempre chame prepare(), mostre uma UI breve de contagem regressiva, depois startActivity(_:).

Chamar end() direto a partir de .running. Pular .stopped pula a janela de finalização de métricas. O builder ao vivo pode não ter processado as amostras finais antes que a sessão termine, levando a estatísticas de resumo ausentes. Correção: sempre chame stopActivity(_:) primeiro, espere o callback do delegate confirmar .stopped, depois chame end().

Inferir estado em vez de usar o delegate. Apps que rastreiam estado local (isWorkoutActive: Bool) e nunca conectam o delegate perdem transições conduzidas pelo sistema (auto-pausa, auto-encerramento ao remover o relógio, estados de erro). Correção: sempre use o delegate como fonte da verdade.

O que esse padrão significa para apps iOS 26+

Três conclusões.

  1. Mapeie o ciclo de vida para o estado da UI explicitamente. A UI de um app de treino tem estados óbvios: não-iniciado, preparando, ativo, pausado, resumo, concluído. Mapeie cada um para um HKWorkoutSessionState. Não rode a UI a partir de booleanos ad-hoc; vincule-a ao estado reportado da sessão através do delegate.

  2. Use prepare() mais UI de contagem regressiva para qualquer sessão que mostre métricas. O aquecimento de 3 segundos é a diferença entre dados em que o usuário confia e dados que o usuário descarta. O custo é um pequeno elemento de UI; o ganho são métricas confiáveis.

  3. As sessões de treino no iPhone do iOS 26 precisam de código de builder diferente. O API da sessão é compartilhado; o lado do builder é específico de plataforma. Apps que compartilham um caminho de código entre iOS e watchOS precisam de ramos #if os(watchOS) explícitos ou um wrapper que abstraia a diferença.

O cluster completo do Apple Ecosystem: App Intents tipados; servidores MCP; a questão de roteamento; Foundation Models; a distinção LLM entre runtime e tooling; três superfícies; o padrão de fonte única da verdade; Dois servidores MCP; hooks para desenvolvimento Apple; Live Activities; o runtime do watchOS; internos do SwiftUI; o modelo mental espacial do RealityKit; disciplina de schema do SwiftData; padrões de Liquid Glass; shipping multiplataforma; a matriz de plataformas; framework Vision; Symbol Effects; inferência Core ML; API de Writing Tools; Swift Testing; Privacy Manifest; Acessibilidade como plataforma; tipografia SF Pro; padrões espaciais visionOS; framework Speech; migrações SwiftData; focus engine do tvOS; internos de @Observable; protocolo Layout do SwiftUI; SF Symbols customizados; HDR no AVFoundation; sobre o que me recuso a escrever. O hub está na Série Apple Ecosystem. Para contexto mais amplo de iOS com agentes de IA, veja o guia de iOS Agent Development.

FAQ

O iPhone usa o mesmo HKLiveWorkoutBuilder que o Apple Watch?

Sim, desde o iOS 26. O mesmo HKLiveWorkoutBuilder API chega ao iOS 26+, iPadOS 26+, Mac Catalyst 26+ e watchOS 5+. As diferenças de plataforma estão no modelo de runtime e na disponibilidade de sensores, não no API do builder. Treinos no iPhone lidam com privacidade de tela bloqueada e recuperação de crash através do scene delegate (conforme a sessão 322 do WWDC 2025), o que difere da garantia de runtime ao baixar o pulso do watchOS, mas o API de acumulação de dados é o mesmo.

Qual é a duração máxima de um treino?

Não há um limite rígido de duração. Limites práticos vêm da bateria (Apple Watch dura ~6-8 horas sob treino contínuo) e armazenamento (treinos com dados de alta frequência acumulam rapidamente). Apps de corrida de maratona (treinos de 12+ horas) já estão no mercado hoje; o framework os suporta.

Como eu lido com auto-pausa?

Defina HKWorkoutConfiguration.activityType para um que suporte auto-pausa (por exemplo, .running). O watchOS irá automaticamente pausar e retomar com base no movimento do usuário. As transições de estado fluem através do delegate; trate-as da mesma forma que pausas iniciadas pelo usuário.

O que acontece se o usuário tirar o relógio no meio do treino?

A sessão continua em seu estado atual (tipicamente .running). O sistema watchOS eventualmente encerrará a sessão se o relógio estiver fora do pulso por tempo demais; o callback didFailWithError do delegate dispara quando isso acontece. Apps com sessões entre dispositivos (iOS 26+) podem fazer handoff para o iPhone se o usuário tiver ambos os dispositivos.

Devo salvar o treino no HealthKit?

Quase sempre sim. Chamar builder.finishWorkout(completion:) escreve o treino completo no HealthKit, o que significa que os dados aparecem no app Atividade, na lista de treinos do app Saúde e em quaisquer outros apps que o usuário tenha autorizado. Pular o salvamento descarta os dados; o framework não fornece caminho de recuperação.

Como isso se relaciona com outras adições recentes do Apple Health?

iOS 26 / watchOS 26 expandiram o API de treinos de duas formas específicas: primeiro, trazendo HKWorkoutSession para o iPhone (coberto acima); segundo, ampliando a lista de tipos de atividade e a cobertura de auto-detecção. O post do cluster sobre o Contrato de runtime do watchOS cobre o lado de runtime; este post cobre o lado de ciclo de vida. Juntos eles descrevem a superfície completa para entregar um app de treino.

Referências


  1. Documentação do Apple Developer: HKWorkoutSession. A classe de sessão com transições de estado, configuração e o protocolo delegate. 

  2. Documentação do Apple Developer: HKWorkoutSessionState. Os cinco casos de estado (.notStarted, .prepared, .running, .paused, .stopped, .ended) e suas semânticas. 

  3. Documentação do Apple Developer: HKLiveWorkoutBuilder e HKLiveWorkoutDataSource. O API do builder ao vivo (watchOS 5+, iOS 26+, iPadOS 26+, Mac Catalyst 26+) e sua fonte de dados. 

  4. Apple Developer: Track workouts with HealthKit on iOS and iPadOS (sessão 322 do WWDC 2025). A expansão do iOS 26 de HKWorkoutSession para o iPhone. 

  5. Documentação do Apple Developer: HKWorkoutSessionDelegate. O protocolo delegate com transição de estado e callbacks de erro. 

  6. Documentação do Apple Developer: Background Execution on watchOS. O contrato de runtime do watchOS descrevendo quais tipos de sessão mantêm apps rodando ao baixar o pulso. 

Artigos relacionados

HealthKit + SwiftUI on iOS 26: Authorization, Sample Types, and Cross-Platform Patterns

Real production patterns from Water (water tracking, HKQuantitySample) and Return (mindful sessions, HKCategorySample). …

17 min de leitura

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 min de leitura

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 min de leitura