O runtime do watchOS é um contrato, não uma tarefa em segundo plano
Gênero: shipped-code. O post documenta o padrão de runtime do watchOS que o Return entrega em produção. O Return é o timer de meditação em SwiftUI na App Store; o app do Watch executa um timer multiciclos que precisa continuar contando quando o usuário abaixa o pulso.1 O padrão que sobrevive a essa restrição é WKExtendedRuntimeSession mais um delegate global, com escopo de app. Todo o resto morre no momento em que o relógio dorme.
watchOS não é iOS com uma tela menor. O modelo de runtime é diferente. O iOS dá ao app um orçamento generoso em primeiro plano e um runtime em segundo plano que diminui mas é real, através de audio sessions, atualizações de localização, BGTaskScheduler e algumas outras affordances.2 O watchOS dá ao app em primeiro plano um orçamento medido em segundos após o pulso ser abaixado e, depois disso, o app é suspenso, a menos que tenha assinado um contrato de runtime com o sistema. Não existe uma affordance de “estou apenas fazendo uma coisa em segundo plano”. Existe “estou executando um workout, sessão de mindfulness, smart alarm, rota de navegação ou tarefa de monitoramento de saúde”, e nada mais.3
O target Watch do Return é um timer de mindfulness. O contrato de sessão é WKBackgroundModes: mindfulness. O API de runtime é WKExtendedRuntimeSession. O padrão que tirou o app do Watch de quebrado quando o pulso é abaixado para sobrevive a uma meditação de 25 minutos é o que este post descreve.
TL;DR
- O watchOS não tem um background no estilo iOS. O runtime em primeiro plano termina pouco depois do pulso ser abaixado, e apenas tipos de sessão registrados continuam executando.
WKExtendedRuntimeSessioné a superfície API. A sessão precisa declarar um tipo de sessão; para um timer de meditação, o tipo é implícito através deWKBackgroundModes: mindfulnessnoInfo.plist.- O gerenciador de sessão tem que viver no escopo do app, não no escopo da view. O ciclo de vida de view do SwiftUI desaloca objetos pertencentes a views durante a navegação; um delegate de sessão desalocado é uma sessão morta, mesmo que a sessão em si ainda esteja em execução.
- Os callbacks de
WKExtendedRuntimeSessionDelegatesão o contrato:didStart,willExpire,didInvalidateWith. O callback de expiração dispara antes que o sistema force a invalidação; a Apple o descreve como a janela para o app “finalizar e fazer a limpeza”. - Um pulso abaixado sem uma sessão estendida ativa pausa o timer. Um pulso abaixado com uma sessão estendida ativa continua o timer. A sessão é a diferença entre “produto entregue” e “quebrado no segundo uso”.
O problema de background que o watchOS não resolve do jeito do iOS
Apps iOS recorrem a várias affordances de background quando precisam que o app continue executando com a tela apagada:2
- Uma
AVAudioSessioncom a categoria.playbackmantém um app de áudio vivo enquanto a música toca. - Atualizações em background do
CLLocationManagermantêm um app de navegação vivo com uma barra azul. - O
BGTaskSchedulerenfileira trabalho curto de manutenção que o sistema agenda no seu próprio relógio. - Uma extensão de UI em primeiro plano (Live Activity, CallKit, PushKit) faz a ponte entre o processo do app e uma superfície de renderização controlada pelo sistema.
Nada disso ajuda no watchOS da forma que você poderia supor. Apps Watch não têm o mesmo background-task scheduler. Não têm um modo AVAudioSession.playback em segundo plano que mantenha o timer contando em silêncio. Têm uma primitiva estrutural para “quero continuar executando depois que o usuário abaixar o pulso”, e a primitiva é WKExtendedRuntimeSession com um tipo de sessão declarado.3
Os tipos de sessão que a Apple suporta através de WKBackgroundModes são propositalmente restritos:4
workout-processing(comHKWorkoutSessionpara workouts reais)mindfulness(para timers de meditação e exercícios respiratórios)self-care(para rotinas guiadas)physical-therapy(para apps de sessões de terapia)alarm(para alarmes baseados em tempo)underwater-depth(para apps de mergulho e rastreamento de profundidade)
Apps que não se encaixam em uma dessas categorias não podem usar WKExtendedRuntimeSession para executar depois do pulso ser abaixado. Apps de áudio recorrem à categoria mediaPlayback da audio session e à integração com o Now Playing por um caminho de código diferente; apps de navegação usam atualizações em background do CLLocationManager. O Watch não é um computador de propósito geral; é um dispositivo com restrições de bateria que o modelo de runtime aplica.
Um timer de meditação se encaixa em mindfulness. O contrato: declarar o background mode no Info.plist, requisitar uma WKExtendedRuntimeSession, lidar com os callbacks do delegate, encerrar a sessão quando o timer terminar. O sistema concede até cerca de uma hora de runtime por sessão, com a discrição do próprio sistema para encurtar isso sob pressão térmica ou de bateria.3
O padrão que o Return entrega
O padrão começa com a declaração no Info.plist:4
<key>WKBackgroundModes</key>
<array>
<string>mindfulness</string>
</array>
A declaração do mode é o que torna o tipo de sessão válido. Sem ela, chamar WKExtendedRuntimeSession().start() falha silenciosamente e o app suspende quando o pulso é abaixado, exatamente como um app Watch sem nenhum background mode.
O gerenciador de sessão em si tem que viver no escopo do app. O ciclo de vida de view do SwiftUI é hostil a objetos com estado de longa duração: @StateObject e @State têm escopo na view que os possui, e um navigation push que substitui a view descarta o estado junto. Uma WKExtendedRuntimeSession cujo delegate é desalocado no meio da sessão não trava; a sessão continua executando, mas os callbacks do delegate (willExpire, didInvalidateWith) chegam a um objeto liberado, o que significa que a limpeza nunca acontece, o que significa que a próxima chamada de startSession() pensa que não existe sessão ativa e inicia uma duplicada.
O padrão entregue é um singleton em escopo de app. O snippet abaixo é a forma estrutural; em produção, adiciona-se logging dentro de cada método para observabilidade:
import SwiftUI
import WatchKit
final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate {
static let shared = WatchSessionManager()
private var session: WKExtendedRuntimeSession?
private override init() {
super.init()
}
var isSessionActive: Bool {
session != nil
}
func startSession() {
guard session == nil else { return }
let newSession = WKExtendedRuntimeSession()
newSession.delegate = self
newSession.start()
session = newSession
}
func endSession() {
guard let existing = session else { return }
existing.invalidate()
session = nil
}
// MARK: - WKExtendedRuntimeSessionDelegate
func extendedRuntimeSessionDidStart(_ session: WKExtendedRuntimeSession) {}
func extendedRuntimeSessionWillExpire(_ session: WKExtendedRuntimeSession) {
// Apple's "about to expire / finish and clean up" hook
}
func extendedRuntimeSession(
_ session: WKExtendedRuntimeSession,
didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
error: Error?
) {
self.session = nil
}
}
Três detalhes sobre esse singleton que a documentação não menciona:
O static let shared mais @State private var sessionManager = WatchSessionManager.shared no nível @main App mantém o gerenciador vivo durante o ciclo de vida do processo do app no Watch. O SwiftUI não retém singletons só porque uma view os mantém; o binding acima é o que diz ao runtime para manter a referência. Sem o binding em nível de App, o ARC pode descartar o gerenciador quando nenhuma view o segura.
A propriedade session é a proteção contra sessões duplicadas. Um timer com um botão “começar de novo” pode chamar startSession() de múltiplos caminhos; a verificação guard session == nil é o lock. Duas sessões estendidas concorrentes causam comportamento imprevisível: às vezes a segunda tem sucesso e a primeira fica órfã, às vezes a chamada de start falha silenciosamente. O invariante de sessão única previne a classe inteira.
Os callbacks do delegate fazem log mas raramente agem. O callback didStart dispara uma vez por sessão e é um hook útil para observabilidade; o callback willExpire dispara antes que o sistema force a invalidação e é onde a Apple espera que o app “finalize e faça a limpeza”; o callback didInvalidateWith é onde a referência da sessão é limpa para que a próxima chamada de startSession() funcione. O padrão em produção é callbacks atualizam estado, a state machine faz o trabalho, não callbacks fazem o trabalho diretamente.
O gerenciador do timer chama o gerenciador de sessão em toda transição que muda se o timer está ativamente contando:
final class WatchTimerManager: ObservableObject {
func start() {
startExtendedSession() // -> WatchSessionManager.shared.startSession()
// ... start the timer state machine ...
}
func pause() {
timer?.invalidate()
isRunning = false
endExtendedSession() // -> WatchSessionManager.shared.endSession()
}
func reset() {
// ... clear timer state ...
endExtendedSession()
}
private func completeCycle() {
// ... last cycle handling ...
endExtendedSession() // ends on final completion
}
}
A sessão termina ao pausar, ao resetar e na conclusão do ciclo final. O raciocínio do produto: uma meditação pausada não precisa continuar reivindicando o orçamento de runtime que o sistema concede sob mindfulness; retomar do pause readquire uma sessão nova. O custo do produto é que um pause com pulso abaixado não pode ser retomado apenas levantando o pulso; o usuário tem que trazer o app de volta ao primeiro plano para retomar. O ganho do produto é que o custo de bateria do timer pausado vai a zero e o sistema não vê uma sessão obsoleta.
O pulso abaixado é o teste
Testar watchOS no simulador é uma ficção educada. O simulador não impõe o modelo de runtime de pulso abaixado da forma que um Apple Watch real impõe. O simulador mantém o app em primeiro plano enquanto a janela do simulador tiver foco; uma sessão de runtime estendida no simulador parece idêntica a nenhuma sessão, porque o app em primeiro plano continua executando de qualquer forma.
O teste real é num Apple Watch de verdade:5
- Iniciar o timer.
- Abaixar o pulso (ou pressionar o botão lateral para travar a tela).
- Aguardar 30 segundos.
- Levantar o pulso de volta.
Sem uma sessão de runtime estendida ativa, o app do Watch fica suspenso; o estado do timer é congelado no momento em que o pulso é abaixado e retoma a partir desse estado congelado. Para uma meditação de 5 minutos em que o usuário fecha os olhos, o bug é invisível até que o timer esteja errado pelo tempo em que os olhos ficaram fechados.
Com uma sessão de runtime estendida ativa, o timer continua contando. O levantar do pulso revela o timer na posição decorrida correta. A sinalização de áudio (se o timer tocar uma na conclusão) dispara no horário de relógio correto, não no horário em que o pulso foi levantado.
O cenário de pulso abaixado foi o bug que o Return entregou na v1 e corrigiu na v2. A correção é o padrão de singleton acima; o bug era uma instância de WatchSessionManager mantida por uma view SwiftUI que era desalocada num navigation push. A sessão estava tecnicamente em execução do lado do sistema, mas o delegate estava liberado; a próxima chamada de start de sessão era silenciosamente um no-op porque a propriedade session do gerenciador havia sido definida em um objeto agora morto. Testes em dispositivo real expõem a falha em segundos. Testes no simulador nunca expõem.
O que os callbacks do delegate realmente dizem
WKExtendedRuntimeSessionInvalidationReason enumera as formas como uma sessão termina:6
| Razão | Quando acontece |
|---|---|
none |
A sessão foi explicitamente invalidada pelo app chamando invalidate() |
sessionInProgress |
Uma sessão do mesmo tipo já está em execução |
expired |
O limite de tempo imposto pelo sistema foi alcançado |
resignedFrontmost |
Um app diferente ficou em primeiro plano enquanto a sessão executava |
suppressedBySystem |
O sistema suprimiu a sessão (baixa energia, pressão térmica) |
error |
Ocorreu um erro irrecuperável; verifique o parâmetro error |
As razões que importam para o design do produto:
expired significa que o usuário recebeu a sessão completa que você pediu. A sessão correu até seu fim natural. A duração de meditação mais longa do Return é 60 minutos, que está bem na borda do que sessões mindfulness tipicamente recebem. Uma meditação de 90 minutos rotineiramente atingiria expired e o timer morreria no meio da sessão. A decisão de produto é limitar as durações disponíveis ao que o modelo de runtime pode realmente entregar.
resignedFrontmost significa que o usuário abriu outro app do Watch e sua sessão perdeu. Usuários de Watch são ágeis em deslizar para um app diferente e depois esquecer. A decisão de produto é pausar-ao-resignar (estado preservado, usuário pode voltar) ou encerrar-ao-resignar (sessão acabou, usuário recebe um sinal de “você parou cedo”). O Return escolhe pausar-ao-resignar para que o usuário possa atender uma chamada no meio da meditação e voltar.
suppressedBySystem é a versão educada de “o relógio está quente”. Um dispositivo watchOS sob pressão térmica ou bateria baixa pode revogar uma sessão de runtime estendida mesmo sem mau uso do app. O gerenciador de sessão tem que lidar com o caso graciosamente: limpar a referência, exibir um aviso não bloqueante e não entrar num estado em que tente reiniciar uma sessão que o sistema acabou de recusar.
O callback willExpire dispara quando a sessão está prestes a expirar e é documentado como o momento para o app “finalizar e fazer a limpeza”.3 O callback é onde um app pode escrever um snapshot de estado final, tocar uma sinalização de áudio de encerramento ou apresentar uma UI de “sessão terminando em breve”. O Return hoje só faz log do callback; uma limpeza mais rica (entrada de log no HealthKit, fade-out de áudio) acontece nos caminhos de reset e de conclusão do timer e está na lista de o que eu construiria diferente para a janela do willExpire.
O que eu construiria diferente
Duas coisas, se o Return estivesse começando do zero.
Usar HKWorkoutSession para qualquer sessão cujo valor aumente com integração HealthKit. Um timer de meditação fica na borda entre mindfulness e workout-processing. Mindfulness foi a escolha certa para a v1 porque o modelo de dados é mais simples e a expectativa do usuário é “isso é meditação, não um workout”. O HKWorkoutSession carrega uma integração HealthKit mais granular (início de sessão, fim de sessão, segmentos, eventos) e oferece uma interface LiveWorkoutBuilder mais rica para acumular dados. O julgamento arquitetural, não uma garantia documentada da Apple: para um app cujo valor depende de telemetria detalhada de sessão, o caminho de workout-session lida com uma estrutura que WKExtendedRuntimeSession não lida.
Adicionar uma superfície de observabilidade de estado de sessão desde o primeiro dia. A primeira versão do Return logava eventos de sessão no console. A segunda versão adicionou visibilidade de estado de sessão no dispositivo para depuração. A terceira exporia um toggle de modo desenvolvedor que mostra o histórico de razões de sessão para o usuário quando algo dá errado, em vez de tratar a invalidação de sessão como uma caixa-preta. O runtime do watchOS é opaco; a superfície de debug precisa compensar.
Quando WKExtendedRuntimeSession é a resposta errada
Três casos em que o tipo de sessão não se encaixa:
Workouts que precisam de marcadores de segmento, streams de frequência cardíaca ou rastreamento ativo de calorias. Use HKWorkoutSession diretamente com um HKLiveWorkoutBuilder. O API Workout é o caminho documentado da Apple para workouts reais (e meditações caminhadas ou atividade vigorosa); WKExtendedRuntimeSession é o caminho documentado para sessões não-workout como mindfulness ou alarmes. Um app de meditação não precisa de um workout; um app Couch-to-5K precisa.
Reprodução de áudio que precisa de uma superfície Now Playing. Use uma AVAudioSession configurada para playback junto com entitlements de audio session do watchOS; a integração com Now Playing mais a superfície de playback do sistema é o que apps de áudio querem, e o caminho de áudio é totalmente separado de WKExtendedRuntimeSession. WKExtendedRuntimeSession não dá Now Playing nem o roteamento de áudio do sistema.
Sincronização de dados de longa duração sem consciência do usuário. Use WKApplicationRefreshBackgroundTask para janelas periódicas de refresh que o sistema agenda. O usuário não está no app; o app não precisa continuar executando; ele precisa acordar brevemente e atualizar. Os dois modelos, de background-task e de extended-runtime-session, atendem a necessidades muito diferentes.
O que o padrão significa para apps entregando no watchOS 11+
Três conclusões.
-
O modelo de runtime do Watch é opt-in. Escolha um tipo de sessão e viva dentro de suas regras. Apps que tentam fazer “trabalho geral em segundo plano” no watchOS vão perder. Escolha
mindfulness,workout-processing,self-care,physical-therapy,alarmouunderwater-depth, e desenhe a experiência do usuário em torno do orçamento de runtime que vem com o tipo de sessão escolhido. -
O delegate de sessão tem que viver em escopo de app. O ciclo de vida de view do SwiftUI não protege objetos com estado de longa duração. Um singleton
static let sharedvinculado no nível@main Appé o menor padrão que sobrevive a navigation pushes, substituições de view e ao comportamento normal de desalocação do SwiftUI. -
Teste em hardware real. O simulador não impõe o modelo de runtime de pulso abaixado. O bug que um app Watch não consegue testar no simulador é o bug que ele entrega aos usuários.
Combine este post com meus textos anteriores sobre a mesma família de apps: SwiftUI multiplataforma (o Return entrega em iPhone, iPad, Watch, Mac e Apple TV); a state machine das Live Activities (a superfície do lado iOS para o mesmo timer); padrões de HealthKit (onde as sessões de mindfulness do Watch caem nos dados de Saúde do usuário). O conjunto completo está no hub da Apple Ecosystem Series. Para um contexto mais amplo de iOS com agentes de IA, veja o guia de iOS Agent Development.
FAQ
O que é uma extended runtime session do watchOS?
Uma extended runtime session do watchOS (WKExtendedRuntimeSession) é o API que um app Watch usa para continuar executando depois que o usuário abaixa o pulso. A sessão precisa declarar um tipo (mindfulness, workout-processing, alarm, etc.) através de WKBackgroundModes no Info.plist. Sem uma sessão estendida ativa, o watchOS suspende o app pouco depois que o pulso é abaixado.
Por que meu timer no watchOS para de contar quando o usuário abaixa o pulso?
O app do Watch suspende pouco depois do pulso ser abaixado, a menos que uma WKExtendedRuntimeSession ativa de um tipo suportado esteja em execução. Um gerenciador de timer que não inicia tal sessão verá seu runtime em background ser cortado, e o estado do timer congela no momento em que o pulso é abaixado até que o usuário levante o pulso novamente.
Qual é a diferença entre WKExtendedRuntimeSession e HKWorkoutSession?
WKExtendedRuntimeSession é o API de runtime estendido de propósito geral para sessões não-workout como mindfulness, alarm ou self-care. HKWorkoutSession é o API para workouts reais; ele se integra com HealthKit, suporta marcadores de segmento e é o caminho documentado para meditações caminhadas ou atividade vigorosa. Apps de mindfulness sem telemetria de nível workout usam o primeiro; apps de workout usam o segundo.
O sistema pode revogar minha extended runtime session?
Sim. WKExtendedRuntimeSessionInvalidationReason inclui expired (limite de tempo do sistema atingido), resignedFrontmost (outro app Watch ficou em primeiro plano) e suppressedBySystem (baixa energia ou pressão térmica). O gerenciador de sessão tem que lidar com cada um de forma limpa: a referência é limpa, o estado do timer reage adequadamente e a próxima chamada de início de sessão funciona corretamente.
Onde o gerenciador de sessão deve viver em um app watchOS SwiftUI?
Em escopo de app, como um singleton vinculado a partir da struct @main App. O estado em escopo de view do SwiftUI (@State, @StateObject) é desalocado em navigation pushes, substituições de view ou quando o app vai para o background. Um delegate de sessão pertencente a uma view que é liberado no meio da sessão faz com que a referência da sessão vaze e impede que sessões subsequentes iniciem corretamente.
Referências
-
O Return do autor, um timer de meditação em SwiftUI publicado na App Store em 21 de abril de 2026, disponível para iPhone, iPad, Mac, Apple Watch e Apple TV. O app do Watch usa
WKExtendedRuntimeSessioncom background modemindfulnesspara o runtime do timer multiciclos. ↩ -
Apple Developer, “About the background execution sequence”. Affordances de runtime em background do lado iOS (audio sessions, location, BGTaskScheduler) e como elas diferem do watchOS. ↩↩
-
Apple Developer, “WKExtendedRuntimeSession”. Tipos de sessão, ciclo de vida, callbacks do delegate, limites de runtime e a chave
WKBackgroundModesdo Info.plist. ↩↩↩↩ -
Apple Developer, “Information Property List: WKBackgroundModes”. Strings de tipo de sessão suportadas:
workout-processing,mindfulness,self-care,physical-therapy,alarm,underwater-depth. ↩↩ -
Apple Developer, “Building a watchOS app” e a orientação de testes do WatchKit. O comportamento de runtime em dispositivo real não é reproduzível no simulador do watchOS; o simulador não impõe a suspensão por pulso abaixado. ↩
-
Apple Developer, “WKExtendedRuntimeSessionInvalidationReason”. Casos da enumeração:
none,sessionInProgress,expired,resignedFrontmost,suppressedBySystem,error. ↩