Sitemap
Liviabion

Liviabion

Construindo uma Live Activity com SwiftUI e ActivityKit

Aprenda a implementar uma Live Activity do zero utilizando um cronômetro como exemplo.

15 min readOct 22, 2026

--

Imagine estar no meio de uma corrida e precisar desbloquear o celular e abrir um aplicativo apenas para conferir há quanto tempo você está correndo. Não seria mais prático acompanhar essas informações diretamente pela tela bloqueada?

Foi pensando em proporcionar uma experiência mais fluida como essa que, durante o desenvolvimento do RUN2U, um aplicativo iOS voltado à criação de rotas personalizadas e ao acompanhamento de corridas, surgiu a necessidade de implementar uma Live Activity.

As Live Activities permitem exibir informações atualizadas de uma atividade em andamento diretamente na Tela Bloqueada e na Dynamic Island dos dispositivos compatíveis, oferecendo acesso rápido a informações relevantes sem a necessidade de abrir o aplicativo.

No RUN2U, implementei essa funcionalidade para que os usuários pudessem acompanhar o progresso de suas corridas de maneira mais prática e integrada ao iOS.

Press enter or click to view image in full size

Se quiser saber mais sobre o aplicativo e ver essa funcionalidade na prática, o RUN2U está disponível na App Store

Depois dessa experiência, resolvi compartilhar o que aprendi durante a implementação e mostrar como criar uma Live Activity do zero utilizando SwiftUI e ActivityKit.

Para isso, escolhi um exemplo mais simples: um cronômetro. Assim, consigo focar nos principais conceitos sem precisar abordar toda a complexidade de um aplicativo de corrida.

Ao longo deste artigo, vou mostrar como configurar o projeto, construir a interface da Live Activity e gerenciar seu ciclo de vida, desde a inicialização até o encerramento.

Minha ideia é que, ao final, você não tenha apenas um cronômetro funcional, mas também entenda como utilizar Live Activities em seus próprios projetos iOS.

O que vamos construir?

Para explorar o funcionamento das Live Activities de forma prática, decidi criar um aplicativo simples de cronômetro utilizando SwiftUI. A ideia é partir de um projeto já funcional e adicionar uma Live Activity com ActivityKit, permitindo acompanhar o tempo decorrido diretamente na Tela Bloqueada e na Dynamic Island.

Press enter or click to view image in full size

Para facilitar o acompanhamento, disponibilizei o código inicial no GitHub. O projeto já contém a implementação básica do cronômetro, então podemos focar na criação e a integração da Live Activity.

Acessar o projeto inicial no GitHub

Também vou explorar como utilizar os recursos do próprio SwiftUI para exibir um cronômetro que continua avançando visualmente sem precisar enviar atualizações a cada segundo.

Configurando o projeto

Agora que já sabemos o que vamos construir, precisamos preparar nosso projeto para oferecer suporte a Live Activities.

1. Obtendo o projeto inicial

Se você quiser acompanhar a implementação utilizando o mesmo código, basta acessar o repositório do projeto e selecionar a branch starter.

Também é possível clonar o repositório diretamente pelo terminal:

git clone -b starter https://github.com/liviabion/Timer.git

Com o projeto no seu computador, abra-o no Xcode e execute o aplicativo para conferir o funcionamento do cronômetro.

A partir daqui, vou utilizar essa implementação como ponto de partida. Caso você já tenha um projeto SwiftUI, também poderá acompanhar os próximos passos para adicionar uma Live Activity ao seu aplicativo.

2. Adicionando uma Widget Extension

O primeiro passo é adicionar uma Widget Extension ao projeto.

Embora uma Live Activity não seja exatamente um widget tradicional, sua interface é implementada utilizando SwiftUI e WidgetKit dentro de uma extensão. Já o ActivityKit fica responsável por gerenciar o ciclo de vida da atividade.

Para criar essa extensão, no Xcode:

  1. Acesse File → New → Target.
  2. Procure por Widget Extension, selecione e aperte em Next.
  3. Escolha um nome para a extensão, como "TimerLiveActivity".
  4. Marque a opção Include Live Activity.
  5. Clique em Finish.
Press enter or click to view image in full size

Após finalizar a criação da extensão, o Xcode adicionará um novo conjunto de arquivos ao projeto, separado dos arquivos do aplicativo principal.

Entre os arquivos criados, podemos destacar o TimerLiveActivityLiveActivity.swift, responsável pela definição da interface da Live Activity, e o TimerLiveActivityBundle.swift, que registra os componentes da extensão.

Por enquanto, não precisamos modificar esses arquivos. Vou explicar suas responsabilidades e como utilizá-los ao longo da implementação.

3. Habilitando o suporte a Live Activities

Com a extensão criada, precisamos informar ao iOS que nosso aplicativo oferece suporte a Live Activities.

Para isso, selecione o target principal do aplicativo, acesse a aba Info e adicione a seguinte propriedade:

  • Key: Supports Live Activities
  • Type: Boolean
  • Value: YES

Essa propriedade corresponde à chave NSSupportsLiveActivities no Info.plist.

Press enter or click to view image in full size

Com a Widget Extension criada e o suporte a Live Activities habilitado, concluímos a configuração inicial do projeto

Agora podemos começar a trabalhar com o ActivityKit e entender como definir as informações que serão compartilhadas entre o aplicativo e a Live Activity.

Entendendo a estrutura de uma Live Activity

Agora que o projeto está configurado, precisamos entender como o aplicativo se comunica com a Live Activity e quais informações serão utilizadas para construir sua interface.

Para isso, vamos utilizar o ActivityKit, framework da Apple responsável por gerenciar o ciclo de vida das Live Activities, permitindo iniciar, atualizar e encerrar uma atividade.

Um dos principais componentes desse processo é o protocolo ActivityAttributes, que define os dados associados à atividade.

Esses dados são divididos em duas categorias:

  • Dados estáticos: informações que permanecem constantes durante a execução da Live Activity.
  • Dados dinâmicos: informações que podem mudar ao longo da atividade, representadas por uma estrutura chamada ContentState.

No nosso cronômetro, por exemplo, podemos utilizar um nome como informação estática e armazenar informações relacionadas à contagem do tempo no estado dinâmico.

Para representar esses dados, vou criar uma estrutura que será compartilhada entre o aplicativo principal e a Widget Extension.

Definindo os dados da Live Activity

Para representar as informações do nosso cronômetro, criei um arquivo chamado TimerActivityAttributes.swift dentro de um grupo Shared, garantindo que ele estivesse associado tanto ao target principal do aplicativo quanto ao da Widget Extension.

Press enter or click to view image in full size

Essa configuração é importante porque os dois componentes precisam ter acesso à mesma definição de dados.

import Foundation
import ActivityKit

struct TimerActivityAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
let accumulatedTime: TimeInterval
let startedAt: Date?
let isRunning: Bool
}

let name: String
}

Nesse exemplo, name representa um dado estático, enquanto ContentState reúne as informações que podem mudar ao longo da execução:

  • accumulatedTime: armazena o tempo já contabilizado.
  • startedAt: registra quando a contagem atual começou.
  • isRunning: indica se o cronômetro está em execução.

Optei por essa estrutura para permitir que o cronômetro seja pausado e retomado sem perder o tempo acumulado.

Dessa forma, não precisamos enviar atualizações à Live Activity a cada segundo. Podemos atualizar seu estado apenas quando necessário e utilizar os recursos do SwiftUI para apresentar a passagem do tempo.

Construindo a interface da Live Activity

Com os dados definidos, podemos começar a construir a interface da nossa Live Activity.

Para isso, vou utilizar o arquivo TimerLiveActivityLiveActivity.swift, criado automaticamente pelo Xcode quando adicionamos a Widget Extension ao projeto.

Nesse arquivo, encontramos o ActivityConfiguration, componente do WidgetKit responsável por definir como a Live Activity será apresentada na Tela Bloqueada e na Dynamic Island.

Sua estrutura básica é:

ActivityConfiguration(
for: TimerActivityAttributes.self
) { context in
// Interface da Tela Bloqueada
} dynamicIsland: { context in
// Interface da Dynamic Island
}

O parâmetro context permite acessar as informações definidas anteriormente em TimerActivityAttributes:

  • context.attributes: fornece os dados estáticos, como o nome do cronômetro.
  • context.state: fornece o estado dinâmico mais recente, como o tempo acumulado e a informação de que o cronômetro está ativo ou pausado.

Vou começar pela interface da Tela Bloqueada e deixar a implementação completa da Dynamic Island para a próxima etapa.

Criando a interface da Tela Bloqueada

Para manter o código organizado, vou criar uma View chamada TimerLiveActivityView, responsável por apresentar o nome, o tempo decorrido e o estado atual do cronômetro.

import ActivityKit
import SwiftUI
import WidgetKit

struct TimerLiveActivityView: View {
let attributes: TimerActivityAttributes
let state: TimerActivityAttributes.ContentState
let foregroundColor: Color

var body: some View {
VStack(spacing: 8) {
Text(attributes.name)
.font(.headline)

elapsedTime
.frame(maxWidth: .infinity, alignment: .center)
.multilineTextAlignment(.center)

Text(state.isRunning ? "Em andamento" : "Pausado")
.font(.caption)
}
.padding()
.foregroundStyle(foregroundColor)
}

A interface utiliza os dados recebidos por attributes e state para apresentar as informações em uma VStack: primeiro o nome, depois o tempo decorrido e, por último, o estado atual.

Observe que o tempo é exibido por meio de elapsedTime, uma propriedade que ainda vamos implementar. Separei essa lógica porque a apresentação do tempo precisa se comportar de maneiras diferentes quando o cronômetro está em execução ou pausado.

Exibindo o tempo decorrido

Um dos principais desafios de implementar um cronômetro em uma Live Activity é manter a contagem visível mesmo quando o aplicativo não está em primeiro plano.

Para resolver isso, vou utilizar um recurso do próprio SwiftUI que permite apresentar um temporizador a partir de uma referência temporal.

Dentro de TimerLiveActivityView, adicionarei a seguinte propriedade:

@ViewBuilder
private var elapsedTime: some View {
if state.isRunning, let startedAt = state.startedAt {
let referenceDate = startedAt.addingTimeInterval(
-state.accumulatedTime
)

Text(
timerInterval: referenceDate...Date.distantFuture,
countsDown: false,
showsHours: true
)
.font(.title)
.monospacedDigit()
} else if state.accumulatedTime >= 3_600 {
Text(
Duration.seconds(state.accumulatedTime),
format: .time(
pattern: .hourMinuteSecond(padHourToLength: 1)
)
)
.font(.title)
.monospacedDigit()
} else {
Text(
Duration.seconds(state.accumulatedTime),
format: .time(
pattern: .minuteSecond(padMinuteToLength: 1)
)
)
.font(.title)
.monospacedDigit()
}
}

O @ViewBuilder permite apresentar diferentes conteúdos de acordo com o estado do cronômetro.

  • Quando o cronômetro está em execução

Primeiro, verifico se a contagem está ativa e se existe uma data de início.

Em seguida, calculo uma data de referência:

let referenceDate = startedAt.addingTimeInterval(
-state.accumulatedTime
)

Esse cálculo é importante porque o cronômetro pode ter sido pausado e retomado.

Imagine que ele tenha acumulado 30 segundos antes de ser pausado. Ao retomar a contagem, utilizar apenas startedAt faria o tempo exibido começar novamente do zero.

Por isso, desconto o tempo acumulado da data de início atual. Assim, a interface considera tanto os segundos anteriores à pausa quanto o tempo transcorrido desde a retomada.

Depois, utilizo o inicializador Text(timerInterval:countsDown:) para apresentar a contagem:

Text(
timerInterval: referenceDate...Date.distantFuture,
countsDown: false,
showsHours: true
)

Com countsDown: false, o tempo é apresentado de forma progressiva.

O mais interessante dessa abordagem é que não precisamos enviar atualizações à Live Activity a cada segundo. A partir da referência temporal, o próprio sistema consegue atualizar visualmente o cronômetro.

  • Quando o cronômetro está pausado

Nesse caso, o tempo precisa permanecer estático. Por isso, apresento diretamente o valor armazenado em accumulatedTime, formatado em horas, minutos e segundos.

Get Liviabion’s stories in your inbox

Join Medium for free to get updates from this writer.

Também utilizo .monospacedDigit() para manter a largura dos números consistente e evitar mudanças no alinhamento conforme os dígitos são atualizados.

Dessa forma, conseguimos representar os dois estados do cronômetro utilizando as mesmas informações definidas anteriormente em ContentState.

Integrando a View ao ActivityConfiguration

Com a interface criada, agora precisamos utilizá-la dentro do ActivityConfiguration.

No arquivo TimerLiveActivityLiveActivity.swift, vou configurar a apresentação da Tela Bloqueada utilizando a TimerLiveActivityView:

struct TimerLiveActivityLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerActivityAttributes.self) { context in
TimerLiveActivityView(
attributes: context.attributes,
state: context.state,
foregroundColor: .black
)
.activityBackgroundTint(.white)
.activitySystemActionForegroundColor(.black)
} dynamicIsland: { _ in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
EmptyView()
}
} compactLeading: {
EmptyView()
} compactTrailing: {
EmptyView()
} minimal: {
EmptyView()
}
}
}
}

Aqui, passo os dados de context.attributes e context.state diretamente para a TimerLiveActivityView, permitindo que ela apresente as informações atuais do cronômetro.

Também utilizo .activityBackgroundTint(.white) para definir o fundo da Live Activity na Tela Bloqueada e .activitySystemActionForegroundColor(.black) para personalizar a cor das ações fornecidas pelo sistema.

Press enter or click to view image in full size

Por enquanto, deixei as regiões da Dynamic Island com EmptyView(), apenas para manter sua configuração inicial enquanto desenvolvo a interface da Tela Bloqueada.

Na próxima etapa, vou substituir essas regiões vazias pelas apresentações expandida, compacta e mínima da Dynamic Island.

Personalizando a Dynamic Island

Com a interface da Tela Bloqueada pronta, podemos adaptar nossa Live Activity para a Dynamic Island.

Diferentemente da Tela Bloqueada, a Dynamic Island possui diferentes formas de apresentação, dependendo do espaço disponível e da quantidade de atividades em execução.

O WidgetKit permite configurar três apresentações principais:

  • Expandida: oferece mais espaço para apresentar informações detalhadas.
  • Compacta: divide o conteúdo em duas regiões, uma à esquerda e outra à direita.
  • Mínima: apresenta uma versão reduzida da atividade, geralmente quando mais de uma Live Activity está sendo exibida.

Para nosso cronômetro, vou priorizar o tempo decorrido e adaptar a quantidade de informações ao espaço disponível.

Implementando as apresentações

No arquivo TimerLiveActivityLiveActivity.swift, vou substituir a implementação provisória da Dynamic Island pelo seguinte código:

dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
TimerLiveActivityView(
attributes: context.attributes,
state: context.state,
foregroundColor: .white
)
}
} compactLeading: {
Image(systemName: "stopwatch")
} compactTrailing: {
Text(context.state.isRunning ? "Ativo" : "Pausa")
.font(.caption2)
} minimal: {
Image(systemName: "stopwatch")
}
.keylineTint(.primary)
}

Na apresentação expandida, reutilizo a TimerLiveActivityView para mostrar as informações do cronômetro.

Na apresentação compacta, mantenho apenas o ícone e o estado atual, evitando sobrecarregar o espaço reduzido.

Já na apresentação mínima, utilizo somente o ícone do cronômetro para identificar a atividade.

O modificador .keylineTint(.primary) define a cor de destaque do contorno da Dynamic Island.

Press enter or click to view image in full size

Com isso, temos as primeiras apresentações da nossa Live Activity tanto na Tela Bloqueada quanto na Dynamic Island.

No entanto, até agora trabalhamos apenas com a construção da interface. Para que ela apareça durante a execução do aplicativo, precisamos conectar o cronômetro ao ActivityKit.

Na próxima etapa, vou mostrar como iniciar, atualizar e encerrar uma Live Activity a partir das ações realizadas no aplicativo.

Integrando a Live Activity ao cronômetro

Agora que construímos as interfaces da Tela Bloqueada e da Dynamic Island, precisamos conectar nossa Live Activity ao aplicativo principal.

Até aqui, definimos como as informações serão apresentadas, mas ainda não implementamos os comandos responsáveis por iniciar, atualizar e encerrar a atividade.

Para isso, vou utilizar três operações do ActivityKit:

  • Activity.request(): inicia uma nova Live Activity.
  • activity.update(): atualiza o estado de uma atividade existente.
  • activity.end(): encerra a atividade.

Como nosso cronômetro já possui sua própria lógica de contagem, não precisamos modificá-la. Vou apenas sincronizar as mudanças de estado com o ActivityKit.

Criando um gerenciador para a Live Activity

Para separar essa responsabilidade da interface principal, vou criar um arquivo chamado TimerLiveActivityManager.swift, associado apenas ao target do aplicativo Timer.

Nesse arquivo, vou implementar uma classe responsável por gerenciar a Live Activity.

import ActivityKit
import Foundation

@MainActor
final class TimerLiveActivityManager {
private var activity: Activity<TimerActivityAttributes>? =
Activity<TimerActivityAttributes>.activities.first

func start(with stopwatch: StopwatchState) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
return
}

guard activity == nil else {
return
}

let attributes = TimerActivityAttributes(
name: "Meu cronômetro"
)

let content = ActivityContent(
state: makeContentState(from: stopwatch),
staleDate: nil
)

do {
activity = try Activity.request(
attributes: attributes,
content: content,
pushType: nil
)
} catch {
print("Erro ao iniciar Live Activity: \(error)")
}
}

func startOrResume(with stopwatch: StopwatchState) async {
if resolveActivity() == nil {
start(with: stopwatch)
} else {
await update(with: stopwatch)
}
}

func update(with stopwatch: StopwatchState) async {
guard let activity = resolveActivity() else {
return
}

await activity.update(
ActivityContent(
state: makeContentState(from: stopwatch),
staleDate: nil
)
)
}

func end(with stopwatch: StopwatchState) async {
guard let activity = resolveActivity() else {
return
}

await activity.end(
ActivityContent(
state: makeContentState(from: stopwatch),
staleDate: nil
),
dismissalPolicy: .immediate
)

self.activity = nil
}

private func resolveActivity()
-> Activity<TimerActivityAttributes>? {
if let activity, activity.activityState == .active {
return activity
}

activity =
Activity<TimerActivityAttributes>.activities.first

return activity
}

private func makeContentState(
from stopwatch: StopwatchState
) -> TimerActivityAttributes.ContentState {
TimerActivityAttributes.ContentState(
accumulatedTime: stopwatch.accumulatedTime,
startedAt: stopwatch.startedAt,
isRunning: stopwatch.isRunning
)
}
}

A classe mantém uma referência à Live Activity criada, permitindo que o aplicativo envie atualizações e encerre a mesma atividade posteriormente.

Para evitar repetir código, utilizei o método makeContentState, que transforma as informações do nosso StopwatchState em um ContentState compatível com o ActivityKit.

Assim, utilizamos uma única fonte de informações para representar o estado do cronômetro.

Gerenciando o ciclo de vida da Live Activity

Com o TimerLiveActivityManager implementado, podemos controlar as principais etapas do ciclo de vida da nossa Live Activity.

Iniciando a atividade

Antes de iniciar, verifico se o dispositivo permite utilizar Live Activities por meio de ActivityAuthorizationInfo().areActivitiesEnabled.

Se o recurso estiver disponível, utilizo Activity.request() para criar a atividade com os atributos e o estado inicial do cronômetro:

activity = try Activity.request(
attributes: attributes,
content: content,
pushType: nil
)

Nesse exemplo, utilizo pushType: nil porque as atualizações serão realizadas localmente pelo aplicativo, sem a necessidade de configurar notificações push ou um servidor.

Atualizando a atividade

Quando o usuário pausa ou retoma o cronômetro, utilizo activity.update() para enviar um novo estado:

await activity.update(
ActivityContent(
state: makeContentState(from: stopwatch),
staleDate: nil
)
)

O método makeContentState(from:), definido anteriormente no gerenciador, transforma o estado atual do cronômetro em um ContentState compatível com o ActivityKit.

Essa atualização ocorre quando o estado do cronômetro muda, e não a cada segundo. Enquanto a contagem está em andamento, o SwiftUI continua responsável por apresentar a passagem do tempo a partir da referência temporal que definimos anteriormente.

Encerrando a atividade

Por fim, quando o usuário encerra o cronômetro, utilizo activity.end():

await activity.end(
ActivityContent(
state: makeContentState(from: stopwatch),
staleDate: nil
),
dismissalPolicy: .immediate
)

Além de enviar o estado final, utilizo .immediate para solicitar que o sistema remova a Live Activity assim que ela for encerrada.

Com isso, já temos os métodos necessários para gerenciar o ciclo de vida da nossa Live Activity. O próximo passo é conectá-los às ações do cronômetro.

Conectando a Live Activity aos botões do cronômetro

Agora vou integrar o TimerLiveActivityManager à ContentView, que já contém os botões para iniciar, pausar e encerrar o cronômetro.

Primeiro, adiciono uma instância do gerenciador:

@State private var liveActivityManager =
TimerLiveActivityManager()

Em seguida, conecto cada ação ao método correspondente.

Iniciar ou retomar

Ao iniciar o cronômetro pela primeira vez, precisamos criar uma nova Live Activity. Caso ele esteja apenas pausado, devemos atualizar a atividade existente.

Para isso, utilizo o método startOrResume(with:), que já implementei no TimerLiveActivityManager.

O método startOrResume(with:) utiliza resolveActivity() para procurar uma Live Activity existente. Caso encontre uma atividade, atualiza seu estado; caso contrário, solicita a criação de uma nova.

Dessa forma, conseguimos reutilizar a mesma Live Activity quando o cronômetro é retomado, sem precisar iniciar outra atividade a cada vez.

No botão Iniciar da ContentView:

Button("Iniciar") {
stopwatch.start(at: .now)
let currentState = stopwatch
    Task {
await liveActivityManager.startOrResume(
with: currentState
)
}
}
.buttonStyle(.borderedProminent)
.disabled(stopwatch.isRunning)

Pausar

Quando o usuário pausa o cronômetro, o StopwatchState registra o tempo decorrido em accumulatedTime e interrompe a contagem.

Depois disso, envio o novo estado para a Live Activity:

Button("Pausar") {
stopwatch.pause(at: .now)
let currentState = stopwatch
    Task {
await liveActivityManager.update(
with: currentState
)
}
}
.buttonStyle(.bordered)
.disabled(!stopwatch.isRunning)

Encerrar

Por último, ao encerrar o cronômetro, também precisamos finalizar a Live Activity.

Antes de redefinir a contagem, crio uma cópia do estado atual e utilizo pause(at:) para registrar o tempo decorrido até aquele momento.

Dessa forma, preservo as informações finais que serão enviadas ao ActivityKit, mesmo depois de executar reset() no cronômetro principal.

Button("Encerrar", role: .destructive) {
var finalState = stopwatch
finalState.pause(at: .now)
    stopwatch.reset()    Task {
await liveActivityManager.end(
with: finalState
)
}
}
.buttonStyle(.bordered)

Com essas alterações, o StopwatchState continua responsável pela lógica da contagem, enquanto o TimerLiveActivityManagergerencia a comunicação com o ActivityKit.

Sempre que o usuário inicia, pausa, retoma ou encerra o cronômetro, o aplicativo comunica a mudança de estado à Live Activity, permitindo que sua interface acompanhe a contagem.

Agora podemos testar o ciclo completo e verificar como a Live Activity se comporta quando o aplicativo deixa de estar em primeiro plano.

Resultado final

Com a implementação concluída, já podemos executar o aplicativo e acompanhar o cronômetro diretamente na Tela Bloqueada e na Dynamic Island.

Ao iniciar a contagem, a Live Activity é criada e passa a apresentar o tempo decorrido. Quando pausamos o cronômetro, o tempo permanece estático e, ao retomá-lo, a contagem continua de onde parou.

Na Dynamic Island, podemos visualizar as apresentações compacta e expandida, com as informações adaptadas ao espaço disponível.

Press enter or click to view image in full size

Ao encerrar o cronômetro, a Live Activity também é finalizada, completando seu ciclo de vida.

Conclusão

Ao longo deste artigo, mostrei como implementar uma Live Activity utilizando SwiftUI, WidgetKit e ActivityKit, desde a configuração inicial do projeto até a integração com um cronômetro funcional.

Com esse exemplo, foi possível explorar o ciclo de vida de uma Live Activity, entender como seus dados são compartilhados entre o aplicativo e a Widget Extension e utilizar os recursos do SwiftUI para apresentar informações atualizadas sem precisar enviar alterações a cada segundo.

Embora tenhamos utilizado um cronômetro simples, os mesmos conceitos podem ser aplicados a diferentes situações, como acompanhar entregas, monitorar atividades físicas ou visualizar o progresso de tarefas.

No meu caso, esse conhecimento foi aplicado ao desenvolvimento do RUN2U, permitindo que os usuários acompanhem suas corridas diretamente pela Tela Bloqueada e pela Dynamic Island.

Espero que este tutorial tenha ajudado você a entender melhor o funcionamento das Live Activities e a explorar novas possibilidades para seus próprios projetos iOS.

O código completo da implementação está disponível no GitHub:

Acessar o projeto no GitHub

Até a próxima!

--