PhaseAnimator vs KeyframeAnimator no SwiftUI
Por equipe Charmotion · Atualizado em

Use PhaseAnimator quando uma animação passa por alguns estados discretos, como repouso, subida e pouso, cada um com sua própria curva. Use KeyframeAnimator quando escala, rotação e deslocamento precisam de trilhas temporizadas independentes. Ambos chegaram no iOS 17, macOS 14, tvOS 17, watchOS 10 e visionOS 1, e ambos podem rodar em loop ou ser acionados por um trigger.
Principais pontos
- O PhaseAnimator percorre uma sequência de fases; o SwiftUI interpola entre elas com a animação que você retorna para cada fase.
- O KeyframeAnimator anima um único tipo de valor ao longo de
KeyframeTracks paralelas com durações explícitas. - Sem um trigger, um phase animator fica em loop para sempre;
keyframeAnimator(initialValue:repeating:…)repete por padrão. - Passe
trigger:para qualquer um deles para tocar uma vez a cada mudança de valor. - Para um personagem com muitos estados em que os designers fazem iterações, uma state machine do Rive é mais adequada do que trilhas codificadas à mão.
O que o PhaseAnimator faz?
O PhaseAnimator (e o modificador .phaseAnimator) recebe uma sequência de fases, renderiza seu conteúdo para a fase atual e anima até a próxima. A assinatura da Apple:
func phaseAnimator<Phase>(
_ phases: some Sequence,
content: @escaping (PlaceholderContentView<Self>, Phase) -> some View,
animation: @escaping (Phase) -> Animation? = { _ in .default }
) -> some View
Um selo que sobe e pousa:
enum BadgePhase: CaseIterable {
case rest, lift, land
}
struct NewBadge: View {
var body: some View {
Image(systemName: "star.fill")
.font(.largeTitle)
.foregroundStyle(.yellow)
.phaseAnimator(BadgePhase.allCases) { content, phase in
content
.scaleEffect(phase == .lift ? 1.2 : 1)
.offset(y: phase == .lift ? -12 : 0)
.rotationEffect(.degrees(phase == .land ? -6 : 0))
} animation: { phase in
switch phase {
case .rest: return .smooth(duration: 0.6)
case .lift: return .spring(duration: 0.3, bounce: 0.4)
case .land: return .bouncy
}
}
}
}
A closure de animação recebe a fase para a qual se está animando. Pense nas fases como poses: você descreve cada pose, e o SwiftUI cuida dos intermediários.
O que o KeyframeAnimator faz?
O KeyframeAnimator anima um único valor, geralmente uma struct com várias propriedades, ao longo de trilhas que você define com tempos explícitos. Cada KeyframeTrack controla um key path e roda em paralelo com as demais.
func keyframeAnimator<Value>(
initialValue: Value,
repeating: Bool = true,
content: @escaping (PlaceholderContentView<Self>, Value) -> some View,
keyframes: @escaping (Value) -> some Keyframes
) -> some View
Um balanço em uma imagem estática de mascote, disparado por um contador:
struct Wobble {
var scale: CGFloat = 1
var rotation: Angle = .zero
var offsetY: CGFloat = 0
}
struct WobblingMascot: View {
@State private var taps = 0
var body: some View {
Image("mascot-still")
.resizable()
.scaledToFit()
.frame(width: 160)
.keyframeAnimator(initialValue: Wobble(), trigger: taps) { content, value in
content
.scaleEffect(value.scale)
.rotationEffect(value.rotation)
.offset(y: value.offsetY)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.15, duration: 0.2)
SpringKeyframe(1.0, duration: 0.3)
}
KeyframeTrack(\.rotation) {
CubicKeyframe(.degrees(-8), duration: 0.15)
CubicKeyframe(.degrees(8), duration: 0.15)
CubicKeyframe(.zero, duration: 0.2)
}
KeyframeTrack(\.offsetY) {
LinearKeyframe(-10, duration: 0.2)
SpringKeyframe(0, duration: 0.3)
}
}
.onTapGesture { taps += 1 }
}
}
Os tipos de keyframe controlam a interpolação: LinearKeyframe, CubicKeyframe, SpringKeyframe e MoveKeyframe (um salto sem interpolação).
Quando usar fases e quando usar keyframes?
| PhaseAnimator | KeyframeAnimator | State machine do Rive | |
|---|---|---|---|
| Modelo mental | Uma lista de poses | Trilhas com tempo definido por propriedade | Rig criado pelo designer, com estados e transições |
| Controle de tempo | Uma curva de animação por fase | Durações exatas por keyframe, por trilha | Definido no editor |
| As propriedades mudam juntas? | Sim, todas mudam a cada fase | Não, as trilhas são independentes | Por bone e camada |
| Quem edita | Desenvolvedor | Desenvolvedor | Designer, controlado por código |
| Ideal para | Pulsos, toques para chamar atenção, loops simples | Balanços, quiques, reações únicas coreografadas | Personagens com muitos humores e reações |
Um teste útil: se você consegue descrever o movimento como "vai para A, depois B, depois volta", use fases. Se a descrição for "a escala passa do ponto em 0,2 s enquanto a rotação oscila duas vezes", use keyframes.

Como fazer cada animator rodar em loop ou por trigger?
Os dois modificadores têm dois modos:
- Contínuo.
.phaseAnimator(phases) { … }sem trigger percorre as fases para sempre..keyframeAnimator(initialValue:repeating:content:keyframes:)se repete enquantorepeatingfortrue, seu valor padrão. - Por trigger.
.phaseAnimator(phases, trigger: value) { … }percorre as fases uma vez sempre quevaluemuda e depois fica parado na primeira fase..keyframeAnimator(initialValue:trigger:content:keyframes:)toca os keyframes uma vez a cada mudança.
// Once per successful save
.phaseAnimator(BadgePhase.allCases, trigger: saveCount) { content, phase in
content.scaleEffect(phase == .lift ? 1.2 : 1)
}
// Repeating idle breathing, paused under Reduce Motion
.keyframeAnimator(initialValue: Wobble(), repeating: !reduceMotion) { content, value in
content.scaleEffect(value.scale)
} keyframes: { _ in
KeyframeTrack(\.scale) {
CubicKeyframe(1.03, duration: 1.2)
CubicKeyframe(1.0, duration: 1.2)
}
}
O valor do trigger precisa ser Equatable; um Int incrementado é a opção mais simples. Loops contínuos devem parar quando o usuário ativar Reduzir Movimento; leia @Environment(\.accessibilityReduceMotion).
Como controlar o tempo dentro de cada fase ou keyframe?
Os dois animators expõem o tempo de formas diferentes, e isso costuma ser o fator decisivo.
PhaseAnimator: a duração de cada fase é o tempo que a Animation retornada leva. Um spring com duration: 0.3 chega à fase em cerca de 0,3 s; um .linear(duration: 1) leva um segundo. Para segurar uma pose, adicione um atraso à animação da fase seguinte:
} animation: { phase in
switch phase {
case .rest: return .easeOut(duration: 0.4).delay(1.5) // pause before lifting again
case .lift: return .spring(duration: 0.3, bounce: 0.4)
case .land: return .bouncy
}
}
Retornar nil para uma fase faz o salto até ela sem animar.
KeyframeAnimator: cada keyframe tem uma duration explícita, e a duração total de uma trilha é a soma dos seus keyframes. As trilhas rodam em paralelo, então a animação termina quando a trilha mais longa termina. Se uma trilha for mais curta, a propriedade dela simplesmente mantém o último valor até as outras terminarem. Para alinhar um momento entre trilhas (por exemplo, o ponto mais baixo de um achatamento em 0,2 s), faça as durações dos keyframes somarem o mesmo tempo em cada trilha.
Como keyframes são dados, você também pode amostrá-los fora de uma view com KeyframeTimeline, o que é útil em testes ou para controlar valores que não são views com a mesma curva.
Quais versões de sistema suportam PhaseAnimator e KeyframeAnimator?
A documentação da Apple informa a mesma disponibilidade para os dois tipos e seus modificadores:
| Plataforma | Mínimo |
|---|---|
| iOS / iPadOS | 17.0 |
| macOS | 14.0 |
| tvOS | 17.0 |
| watchOS | 10.0 |
| visionOS | 1.0 |
Se você ainda suporta o iOS 16, envolva a chamada em if #available(iOS 17, *) e use como alternativa cadeias de withAnimation ou uma view estática. Repare na linha do watchOS: esses animators são uma das poucas formas de animar um personagem no Apple Watch, onde Rive e Lottie não têm suporte oficial (veja animação de personagens no watchOS e visionOS).
Quando uma animação nativa do SwiftUI não basta para um personagem?
Os dois animators transformam a view como um todo: escala, rotação, deslocamento, opacidade, cor. Eles não dobram um braço, não fazem um olho piscar nem sincronizam lábios, porque uma imagem plana não tem partes para mover. Você pode dividir o personagem em imagens em camadas e animar cada uma, mas cada novo estado passa a exigir mais código e mais ajuste manual de tempo.
Mude para um formato criado por designer quando:
- O personagem precisa de mais de dois ou três estados (idle, thinking, success, error, talking…).
- Os estados precisam se misturar no meio da animação, sem corte.
- Quem deve iterar no movimento é um designer, não um desenvolvedor.
- O mesmo personagem também precisa rodar no Android ou na web.
O Rive resolve isso com uma state machine que seu código controla por alguns valores (veja Data Binding do Rive no SwiftUI). Os animators nativos ainda combinam bem com ele: um pulso com phaseAnimator em um selo ao lado de um mascote Rive não custa nada e não precisa de nenhum arquivo.
Perguntas frequentes
PhaseAnimator e KeyframeAnimator rodam no watchOS?
Sim. A Apple lista os dois para watchOS 10 e posterior, junto com iOS 17, macOS 14, tvOS 17 e visionOS 1. Isso faz deles uma forma prática de animar um personagem ou selo simples no Apple Watch, onde os pacotes do Rive e do Lottie não declaram suporte ao watchOS.
Como impedir que um PhaseAnimator fique em loop para sempre?
Passe um valor de trigger: .phaseAnimator(phases, trigger: count). Assim ele percorre as fases uma vez sempre que count muda e fica parado na primeira fase. Sem trigger, o SwiftUI percorre as fases continuamente enquanto a view estiver na tela.
O KeyframeAnimator é igual aos keyframes do CSS?
A ideia é parecida, a estrutura é diferente. Os keyframes do CSS definem todas as propriedades em pontos percentuais. O KeyframeAnimator do SwiftUI dá a cada propriedade sua própria trilha, com durações em segundos e uma interpolação escolhida, então escala e rotação podem seguir tempos diferentes dentro de uma mesma animação. Springs também estão disponíveis como keyframes, algo que o CSS não consegue expressar diretamente.