Pular para o conteúdo
charmotion

PhaseAnimator vs KeyframeAnimator no SwiftUI

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de iOS e Swift: PhaseAnimator vs KeyframeAnimator no SwiftUI, com personagens de demonstração do Charmotion

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?

PhaseAnimatorKeyframeAnimatorState machine do Rive
Modelo mentalUma lista de posesTrilhas com tempo definido por propriedadeRig criado pelo designer, com estados e transições
Controle de tempoUma curva de animação por faseDurações exatas por keyframe, por trilhaDefinido no editor
As propriedades mudam juntas?Sim, todas mudam a cada faseNão, as trilhas são independentesPor bone e camada
Quem editaDesenvolvedorDesenvolvedorDesigner, controlado por código
Ideal paraPulsos, toques para chamar atenção, loops simplesBalanços, quiques, reações únicas coreografadasPersonagens 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.

Tabela de decisão: APIs nativas para feedback de interface, efeitos de ícone para ícones, uma state machine do Rive para personagens, vídeo para clipes de marketing
Escolha o método de animação conforme a tarefa.

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 enquanto repeating for true, seu valor padrão.
  • Por trigger. .phaseAnimator(phases, trigger: value) { … } percorre as fases uma vez sempre que value muda 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:

PlataformaMínimo
iOS / iPadOS17.0
macOS14.0
tvOS17.0
watchOS10.0
visionOS1.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.

Respostas rápidas deste guia

Fontes

Guias relacionados