Criando animações multi-etapa com PhaseAnimator no SwiftUI
As animações podem ajudar a comunicar mudanças em uma interface, fornecer feedback para interações e chamar atenção para conteúdo importante. Enquanto muitos efeitos envolvem uma única transição entre dois estados, outros são compostos por várias etapas que precisam acontecer em uma ordem específica.
O SwiftUI suporta esse tipo de efeito multi-etapa com animações de fase. Usando o modificador phaseAnimator(), definimos uma sequência de fases, onde cada fase descreve a aparência da visualização em um ponto específico da animação. O SwiftUI passa pelas fases na ordem especificada, animando as mudanças entre elas.
As animações de fase são mais adequadas quando o efeito pode ser expresso como uma série de estados discretos e as propriedades afetadas podem mudar juntas entre eles. Para animações onde as propriedades individuais precisam seguir sequências temporais separadas, o SwiftUI fornece animações por chaves.
Para animações que se encaixam no modelo de fase, o SwiftUI oferece duas maneiras de executar a sequência: continuamente enquanto a visualização está visível ou em resposta a uma mudança em um valor gatilho. Neste post, vamos examinar ambos os variantes e ver como valores de fase e transições por etapa controlam seu comportamento.
# Repetindo animações de fase
As animações de fase repetitivas são úteis para efeitos que devem continuar enquanto uma visualização permanece visível.
Por exemplo, podemos usar uma animação de fase repetitiva para chamar a atenção para um temporizador que terminou. O símbolo do relógio levanta, inclina-se de lado a lado, se estabelece e pausa antes que a sequência comece novamente. A animação continua até que o temporizador seja reconhecido.
Podemos representar os estados individuais do relógio com um enum. Cada caso identifica uma fase na sequência, enquanto propriedades computadas fornecem os valores de aparência associados a essa fase.
private enum TimerPhase: CaseIterable { case resting case lifted case tiltedLeft case tiltedRight case settled var scale: Double { switch self { case .lifted: 1.03 case .resting, .tiltedLeft, .tiltedRight, .settled: 1 } } var rotation: Angle { switch self { case .tiltedLeft: .degrees(-2) case .tiltedRight: .degrees(2) case .resting, .lifted, .settled: .zero } } var verticalOffset: Double { self == .lifted ? -8 : 0 }}Os valores de fase passados para um animador de fase devem conformar-se com Equatable. Um enum sem valores associados ganha essa conformidade automaticamente. Ao também fazer o TimerPhase conformar-se a CaseIterable, podemos usar sua coleção allCases para fornecer as fases na ordem de declaração. O primeiro caso, resting, define a aparência inicial do relógio.
Cada fase descreve a aparência completa da visualização neste ponto na sequência. A fase lifted aumenta a escala e aplica um deslocamento vertical negativo, enquanto as duas fases inclinadas retornam o relógio à sua escala e posição normais com rotação em direções opostas. Propriedades que permanecem inalteradas ainda retornam seus valores para cada fase.
Para aplicar esses valores ao relógio, passamos TimerPhase.allCases para o modificador phaseAnimator(_:content:animation:). Sua closure de conteúdo recebe um proxy para a visualização modificada e a fase atual.
private struct AnimatedClock: View {
var body: some View {
ClockSymbol()
.phaseAnimator(TimerPhase.allCases) { clock, phase in
clock
.scaleEffect(phase.scale)
.rotationEffect(phase.rotation)
.offset(y: phase.verticalOffset)
.shadow(
color: .orange.opacity(phase.shadowOpacity),
radius: phase.shadowRadius,
y: phase.shadowOffset
)
}
}
}Quando AnimatedClock aparece, o SwiftUI renderiza o conteúdo usando a primeira fase. Em seguida, anima as mudanças para lifted, aguarda que essa transição seja concluída e avança para tiltedLeft. O processo continua pelas fases restantes, retornando ao resting após o settled e iniciando a sequência novamente. Sem uma closure de animação explícita, o SwiftUI usa a animação padrão para cada transição.
Podemos fornecer um valor diferente de Animation para cada transição. Neste exemplo, o relógio usa animações de mola para levantar e se estabelecer, animações mais curtas de suavização para as inclinações e uma animação lisa atrasada quando retorna ao repouso.
O fase passada para a clausura animation é o destino da transição. Isso significa que a animação associada com lifted controla como o relógio se move de resting para lifted, enquanto a uma associada com settled controla a transição de tiltedRight para settled.
O SwiftUI avança para a próxima fase apenas após a animação atual ser concluída, então a duração e o atraso de cada transição também determinam o timing da sequência completa. O atraso anexado à animação resting mantém o relógio estático por 1,5 segundos antes que ele se levante novamente.
O animador de fase repetitivo não recebe um valor de ativação. Para encerrar a sequência quando o temporizador é reconhecido, podemos substituir o relógio animado pelo símbolo estático. A mesma apresentação estática pode ser usada quando a configuração Reduce Motion do sistema estiver habilitada.
struct RepeatingTimerView: View {
@Environment(<accessibilityReduceMotion>) private var reduceMotion
@State private var isAcknowledged = false
var body: some View {
Button {
isAcknowledged = true
} label: {
if reduceMotion || isAcknowledged {
ClockSymbol()
} else {
AnimatedClock()
}
}
}
O símbolo estático e o texto circundante continuam a comunicar que o temporizador terminou, então a animação chama atenção para o alerta sem se tornar a única forma de transmitir seu significado.
Animações de fase impulsionadas por eventos
Alguns efeitos multi-etapa devem ser executados apenas quando um evento específico ocorre. Para esses efeitos, podemos associar a animação de fase com um valor de gatilho. Sempre que o valor mudar, o SwiftUI anima a visualização através da sequência de fases.
Para ilustrar esse comportamento, vamos olhar para um exemplo onde uma animação de fase fornece feedback quando um documento é salvo para uso offline. Depois que o botão é pressionado, o documento levanta e então cai na bandeja.
Nós podemos descrever os estados do documento com três fases. A fase ready define sua aparência antes da ação, lifted move-o acima da bandeja com uma rotação ligeira e dropped o move para baixo enquanto reduz sua escala e opacidade.
private enum OfflineSavePhase: CaseIterable {
case ready
case lifted
case dropped
var scale: Double {
switch self {
case .ready: 1
case .lifted: 1.08
case .dropped: 0.72
}
}
var rotation: Angle {
switch self {
case .ready, .dropped: .zero
case .lifted: .degrees(-6)
}
}
var verticalOffset: Double {
switch self {
case .ready: 0
case .lifted: -30
case .dropped: 54
}
}
var opacity: Double {
switch self {
case .ready, .lifted: 1
case .dropped: 0.2
}
}
}A primeira fase tem o mesmo papel que em uma animação de fase repetitiva: ela fornece a aparência inicial da visualização. Para a variante acionada, também fornece a aparência que a visualização retorna após a sequência terminar.
Para executar a sequência em resposta a um evento, usamos o modificador phaseAnimator(_:trigger:content:animation:). Além das fases, essa variante também leva um valor de gatilho que segue Equatable.
As animações de fase funcionam bem para efeitos que podem ser descritos como uma sequência de estados completos da visualização. As variantes repetitivas e acionadas determinam quando a sequência é executada, enquanto a animação associada a cada fase controla como a visualização se move para esse estado. Juntos, essas partes nos permitem criar efeitos multi-etapas sem gerenciar a progressão entre as etapas individuais por nossa conta.Se você está procurando construir uma base sólida em SwiftUI, meu livro SwiftUI Fundamentos mergulha profundamente nos princípios e APIs fundamentais do framework para ajudá-lo a entender como ele funciona por baixo dos panos e a usá-lo efetivamente em seus projetos. E meu novo livro A Maneira SwiftUI ajuda você a adotar padrões recomendados, evitar armadilhas comuns e usar as ferramentas nativas do SwiftUI de forma adequada para trabalhar com o framework em vez de contra ele.
Para mais recursos sobre Swift e SwiftUI, confira meus outros livros e pacotes de livros.

