🎯 Crie Animações de Símbolos no SwiftUI 💻
Você pode pensar que animações são apenas um pequeno bônus para um bom aplicativo. Quer dizer, sim e não! Obviamente, você precisa ter um aplicativo que não esmague e você definitivamente deve ter algum conteúdo interessante dentro do seu aplicativo. No entanto, eu acho que animações fazem uma GRANDE diferença na experiência do usuário.
Neste artigo, vamos focar em animar símbolos SF . A Apple realmente fornece uma lista enorme de símbolos lindos para nós, desenvolvedores, usarmos de graça! (Mais uma vez, outro motivo para eu amar o desenvolvimento de dispositivos Apple!) Se você não tem certeza de qual símbolo faz o quê ou só quer conferir as animações, baixe o SF Symbols 5 e você poderá visualizar todos eles.
Aqui está o que faremos!
Primeiro, daremos uma olhada (super detalhada) em symbolEffect, um modificador para executar animações predefinidas sobre símbolos SF. Nota ! Isso só está disponível para iOS 17+. Se você estiver mirando abaixo disso, ainda terá que criar suas próprias animações do zero!
Especificamente, iremos verificar
Além disso, também compartilharei com vocês como animar símbolos usando variableValue.
Visão geral do efeito do símbolo
Existem 7animações integradas
E podemos agrupar as animações acima em quatro tipos de comportamentos
com cada tipo de comportamento requer uma maneira diferente de anexar o efeito. (O Xcode gritará conosco se não fizermos isso corretamente!)
Vamos primeiro explorar o que temos para cada efeito por tipo de comportamento. Então, verificaremos algumas das opções que podemos adicionar a ele.
Efeitos discretos
Para adicionar um efeito discreto a um símbolo de imagem, usaremos symbolEffect(_:options:value:)um modificador.
import SwiftUI
struct SymbolAnimation: View {
@State private var animationCount = 0
var body: some View {
VStack(spacing: 50) {
Button(action: {
animationCount = animationCount + 1
}, label: {
Text("Animate!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack {
Text("bounce")
Image(systemName: "dog.fill")
.symbolEffect(
.bounce,
value: animationCount
)
}
VStack {
Text("pulse")
Image(systemName: "dog.fill")
.symbolEffect(
.pulse,
value: animationCount
)
}
VStack {
Text("variableColor")
Image(systemName: "dog.fill")
.symbolEffect(
.variableColor,
value: animationCount
)
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Observação !
Se o efeito que estamos tentando adicionar não for um efeito discreto, por exemplo, para scale, não seremos capazes de usar symbolEffect(_:options:value:). Você obterá um Instance method ‘symbolEffect(_:options:value:)’ requires that ‘ScaleSymbolEffect’ conform to ‘DiscreteSymbolEffect’erro.
Efeito indefinido
Um efeito indefinido é criado usando symbolEffect(_:options:isActive:)with isActiveespecificando se o efeito está ativo ou não.
import SwiftUI
struct SymbolAnimation: View {
@State var isAnimating: Bool = false
var body: some View {
VStack(spacing: 50) {
Button(action: {
isAnimating.toggle()
}, label: {
Text(isAnimating ? "Stop!" : "Start")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack {
Text("pulse")
Image(systemName: "dog.fill")
.symbolEffect(
.pulse,
options: .speed(5),
isActive: isAnimating
)
}
VStack {
Text("variableColor")
Image(systemName: "dog.fill")
.symbolEffect(
.variableColor.iterative.reversing,
options: .speed(5),
isActive: isAnimating
)
}
VStack {
Text("scale")
Image(systemName: "dog.fill")
.symbolEffect(
.scale.up,
options: .speed(5),
isActive: isAnimating
)
}
VStack {
Text("appear")
Image(systemName: "dog.fill")
.symbolEffect(
.appear,
options: .speed(5),
isActive: isAnimating
)
}
VStack {
Text("disappear")
Image(systemName: "dog.fill")
.symbolEffect(
.disappear,
options: .speed(5),
isActive: isAnimating
)
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Outra nota !
ao usar scaleo efeito, você tem que especificar um modificador extra, upou downpor exemplo. Se você simplesmente fizer o seguinte
Image(systemName: "dog.fill")
.symbolEffect(
.scale,
options: .speed(5),
isActive: isAnimating
)
Você não verá nenhuma animação acontecendo!
Você deve estar se perguntando por que bounce, appear, disappearsão indefinidos se eles são animados apenas uma vez? (Pelo menos era o que eu estava pensando…)
Não sei a resposta exata, mas aqui está como expliquei para mim mesmo. Esses três são indefinidos, porque não podem se reanimar simplesmente incrementando o valor como o que tínhamos para discreto uma vez. Ou seja, você tem que definir o símbolo de volta ao seu tamanho original (quando a animação parar) para escalá-lo novamente. Você tem que mostrá-lo antes que ele possa desaparecer novamente.
Efeito de transição
Para criar um efeito de transição, em vez de usá-lo symbolEffectdiretamente, passaremos isso como um parâmetro para o transitionmodificador.
import SwiftUI
struct SymbolAnimation: View {
@State var isMoonHidden: Bool = false
var body: some View {
VStack(spacing: 50) {
if !isMoonHidden {
Image(systemName: "moon.stars")
.transition(.symbolEffect(.disappear.down))
}
Button(action: {
isMoonHidden.toggle()
}, label: {
Text(isMoonHidden ? "Show!" : "Hide!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Você deve estar se perguntando como é diferente do appeare disappear com comportamento indefinido que vimos na seção anterior.
Aqui está a comparação.
import SwiftUI
struct SymbolAnimation: View {
@State var isMoonHidden: Bool = false
var body: some View {
VStack(spacing: 50) {
VStack(spacing: 10) {
Text("transition")
if !isMoonHidden {
Image(systemName: "moon.stars")
.transition(.symbolEffect(.disappear.down))
}
}
.padding()
.background(RoundedRectangle(cornerRadius: 16).fill(Color.yellow))
VStack(spacing: 10) {
Text("indefinite")
Image(systemName: "moon.stars")
.symbolEffect(.disappear, isActive: isMoonHidden)
}
.padding()
.background(RoundedRectangle(cornerRadius: 16).fill(Color.yellow))
Button(action: {
isMoonHidden.toggle()
}, label: {
Text(isMoonHidden ? "Show!" : "Hide!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Como você pode ver, com o comportamento de transição, o símbolo é removido completamente da visualização, enquanto o disappearefeito indefinido mantém o símbolo na visualização, mas simplesmente o oculta do usuário!
Transição de conteúdo
Isto é usado quando queremos substituir um símbolo atual por um novo em vez de simplesmente mostrar ou esconder um símbolo. Para animar uma transição de conteúdo, usaremos contentTransitionum modificador symbolEffectcomo o seguinte.
import SwiftUI
struct SymbolAnimation: View {
@State var isMoonHidden: Bool = false
var body: some View {
VStack(spacing: 50) {
Image(systemName: isMoonHidden ? "sun.min" : "moon.stars")
.contentTransition(.symbolEffect(.replace))
Button(action: {
isMoonHidden.toggle()
}, label: {
Text(isMoonHidden ? "Show!" : "Hide!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Você deve estar se perguntando por que não usamos o efeito de transição como o seguinte, por exemplo, para animar a substituição.
if !isMoonHidden {
Image(systemName: "moon.stars")
.transition(.symbolEffect(.disappear.down))
} else {
Image(systemName: "sun.min")
.transition(.symbolEffect(.disappear.down))
}
Você poderia, mas se testar isso, você notará que o efeito aqui é unidirecional, neste caso apenas disappear. A parte de aparecer não é animada.
Você pode combinar vários symbolEffectpara obter um comportamento semelhante, como mostrarei a você um pouco mais tarde, mas isso obviamente requer mais código (e trabalho)!
Personalização
opções
Já o usamos algumas vezes acima. Ao passar o optionsparâmetro para symbolEffect(_:options:isActive:)ou symbolEffect(_:options:value:), há algumas personalizações que podemos fazer aqui.
Primeiro, podemos usar o comando repeatpara especificar os horários em que o efeito será repetido e speedpara controlar a velocidade da animação do efeito.
VStack {
Text("bounce: times 3")
Image(systemName: "dog.fill")
.symbolEffect(
.bounce,
options: .repeat(3),
value: animationCount
)
}
VStack {
Text("bounce: times 3 * speed 3")
Image(systemName: "dog.fill")
.symbolEffect(
.bounce,
options: .repeat(3).speed(3),
value: animationCount
)
}
Recomendados pelo LinkedIn
Há também repeatingopções nonRepeatingpara controlar se queremos que o efeito se repita indefinidamente ou se queremos que ele seja executado apenas uma vez.
Modificador de efeito
Dependendo do efeito que estamos usando, temos alguns modificadores adicionais que podemos anexar a ele. Já o usamos com o scaleefeito acima, mas vamos dar uma olhada mais detalhada no que temos aqui.
bounce, scale,appear, disappear
Para bounce, scale, appeare disappear, temos upe downpara controlar a direção do efeito ou o tamanho do efeito para scale.
Temos então byLayere wholeSymbolpara definir o escopo do efeito, seja se estamos animando camada por camada ou o símbolo inteiro de uma vez. Nota! Isso só é aplicável a símbolos multicamadas, como square.stack.3d.up.
import SwiftUI
struct SymbolAnimation: View {
@State private var animationCount = 0
var body: some View {
VStack(spacing: 50) {
Button(action: {
animationCount = animationCount + 1
}, label: {
Text("Animate!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack {
Text("bounce normal")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.bounce,
value: animationCount
)
}
VStack {
Text("bounce up")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.bounce.up,
value: animationCount
)
}
VStack {
Text("bounce.down")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.bounce.down,
value: animationCount
)
}
VStack {
Text("bounce.byLayer")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.bounce.byLayer,
value: animationCount
)
}
VStack {
Text("bounce.wholeSymbol")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.bounce.wholeSymbol,
value: animationCount
)
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Pulso
Para pulso, temos upe downpara controlar a direção do efeito que pode ser usado da mesma forma que acima! Vou pular minha demonstração aqui!
Cor Variável
Na verdade, esse é um dos efeitos mais poderosos que temos quando animamos símbolos multicamadas.
Vamos verificar os modificadores par por par e compará-los com o que temos quando nenhum modificador é especificado!
VStack {
Text("variableColor normal")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor,
options: .repeat(2).speed(2),
value: animationCount
)
}
VStack {
Text("variableColor.cumulative")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.cumulative,
value: animationCount
)
}
VStack {
Text("variableColor.iterative")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.iterative,
value: animationCount
)
}
VStack {
Text("dimInactiveLayers")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.dimInactiveLayers,
value: animationCount
)
}
VStack {
Text("hideInactiveLayers")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.hideInactiveLayers,
value: animationCount
)
}
VStack {
Text("nonReversing")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.nonReversing,
options: .repeat(2).speed(2),
value: animationCount
)
}
VStack {
Text("reversing")
Image(systemName: "square.stack.3d.up")
.symbolEffect(
.variableColor.reversing,
options: .repeat(2).speed(2),
value: animationCount
)
}
Substituir
Primeiro, temos downUp,offUp e para controlar a direção do efeito. upUp
VStack {
Text("replace normal")
Image(systemName: isMoonHidden ? "sun.min" : "moon.stars")
.contentTransition(.symbolEffect(.replace))
}
VStack {
Text("replace normal")
Image(systemName: isMoonHidden ? "sun.min" : "moon.stars")
.contentTransition(.symbolEffect(.replace.downUp))
}
VStack {
Text("replace normal")
Image(systemName: isMoonHidden ? "sun.min" : "moon.stars")
.contentTransition(.symbolEffect(.replace.offUp))
}
VStack {
Text("replace normal")
Image(systemName: isMoonHidden ? "sun.min" : "moon.stars")
.contentTransition(.symbolEffect(.replace.upUp))
}
Temos então byLayere wholeSymbolque funciona da mesma forma que para bounce, scale, appearance e disappear que tivemos acima.
Hora do bônus!
Agrupamento
Observe que, embora Thought symbolEffectseja um modificador para adicionar efeitos de animação integrados aos símbolos SF, não precisamos anexá-lo diretamente à imagem do símbolo.
Se tivermos vários símbolos, por exemplo, em um HStack, e quisermos animá-los todos juntos com o MESMO efeito, podemos simplesmente fazer
HStack(spacing: 20) {
Image(systemName: "square.stack.3d.up")
Image(systemName: "square.stack.3d.up")
}
.font(.system(size: 60))
.symbolEffect(
.variableColor,
options: .speed(0.5),
isActive: isAnimating
)
em vez de adicionar o modificador a cada um deles individualmente.
Combinar vários SymbolEffect
Lembra como eu disse que combinamos multiple symbolEffectcom transition para atingir um comportamento similar de contentTransition? Vamos usar isso como exemplo.
import SwiftUI
struct SymbolAnimation: View {
@State var isMoonHidden: Bool = false
var body: some View {
VStack(spacing: 50) {
if !isMoonHidden {
Image(systemName: "moon.stars")
.transition(.symbolEffect(.disappear.down).combined(with: .symbolEffect(.appear.up)))
} else {
Image(systemName: "sun.min")
.transition(.symbolEffect(.disappear.down).combined(with: .symbolEffect(.appear.up)))
}
Button(action: {
isMoonHidden.toggle()
}, label: {
Text(isMoonHidden ? "Show!" : "Hide!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Neste caso em que estamos usando symbolEffectwith transition, estaremos encadeando efeitos usando .combined(with:).
Para um tipo discreto ou indefinido, podemos simplesmente adicionar vários symbolEffectmodificadores diretamente ao nosso Image.
import SwiftUI
struct SymbolAnimation: View {
@State var isAnimating: Bool = false
var body: some View {
VStack(spacing: 50) {
Button(action: {
isAnimating.toggle()
}, label: {
Text(isAnimating ? "Stop!" : "Start")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack(spacing: 20) {
Text("scale + variableColor")
Image(systemName: "square.stack.3d.up")
.font(.system(size: 60))
.symbolEffect(
.scale.up,
options: .speed(0.5),
isActive: isAnimating
)
.symbolEffect(
.variableColor,
options: .speed(0.5),
isActive: isAnimating
)
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Efeito sem animação
Pode haver casos específicos em que você queira usar o efeito em si, mas sem nenhuma animação. Para conseguir isso, podemos definir transaction.disablesAnimations = truepara o símbolo que não queremos animar.
import SwiftUI
struct SymbolAnimation: View {
@State var isAnimating: Bool = false
var body: some View {
VStack(spacing: 50) {
Button(action: {
isAnimating.toggle()
}, label: {
Text(isAnimating ? "Stop!" : "Start")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack {
Text("scale")
Image(systemName: "dog.fill")
.symbolEffect(
.scale.up,
options: .speed(0.5),
isActive: isAnimating
)
}
VStack {
Text("scale no animation")
Image(systemName: "dog.fill")
.symbolEffect(
.scale.up,
options: .speed(0.5),
isActive: isAnimating
)
.transaction { transaction in
transaction.disablesAnimations = true
}
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Animar com valorvariável
Esta parte na verdade não tem nada a ver com symbolEffect, mas decidi incluí-la aqui porque é outra maneira super útil de animar símbolos SF.
Esta animação é obtida usando variableValueo parâmetro em Image(systemName:variableValue:). Observe que se o símbolo não suportar valores variáveis, este parâmetro não terá efeito. Você pode usar o aplicativo SF Symbols para procurar quais símbolos suportam valores variáveis.
Aqui está um exemplo rápido de variableValuecomo animar signalLevelum wifi.
import SwiftUI
struct SymbolAnimation: View {
@State var signalLevel: Double = 0.0
var body: some View {
VStack(spacing: 50) {
Button(action: {
signalLevel = signalLevel + 0.3
}, label: {
Text("Increase Level!")
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
VStack {
Text("signalLevel: \(signalLevel)")
Image(systemName: "wifi", variableValue: signalLevel)
}
}
.font(.system(size: 30))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.gray.opacity(0.2))
}
}
Observe que o valor de variableValueé um valor entre 0.0e 1.0que a imagem renderizada pode usar para personalizar sua aparência, se especificado.
Obrigado pela leitura!
É tudo o que tenho para hoje!
Boa animação!