🎯 Crie Animações de Símbolos no SwiftUI 💻

🎯 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

  • cada animação fornecida e os tipos de comportamentos que temos
  • Algumas personalizações que podemos fazer
  • Como combinar múltiplas animações/efeitos
  • Use efeito sem animação

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

  • bounce: aplica um efeito de escala transitório ao símbolo.
  • pulse: diminui a opacidade de todas as camadas no símbolo ou de um subconjunto das camadas no símbolo.
  • variableColor: substitui a opacidade de camadas variáveis no símbolo por um padrão possivelmente repetitivo que se move para cima e possivelmente para baixo nas camadas variáveis. Não tem efeito para imagens de símbolos de cores não variáveis.
  • scale: Um efeito de símbolo que dimensiona imagens de símbolos.
  • appear: torna o símbolo visível como um todo ou um grupo de movimento por vez.
  • disappear:Obviamente, o oposto de appearocultar o símbolo como um todo ou um grupo de movimentos por vez.
  • replace: anima a substituição de uma imagem de símbolo por outra.

E podemos agrupar as animações acima em quatro tipos de comportamentos

  • discrete: a animação é executada e termina.
  • indefinite: a animação continuará até ser desativada ou removida.
  • transition: anima um símbolo dentro ou fora da visualização.
  • contentTransition: anima a substituição de um símbolo por outro.

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
        )
}        

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!

  • cumulative: cada camada será animada em sequência, e cada camada permanecerá até que a animação seja concluída.
  • iterative: anima uma camada por vez e desabilita a camada até que a animação seja concluída.

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
        )
}        

  • dimInactiveLayers: camadas inativas ficarão esmaecidas até se tornarem ativas.
  • hideInactiveLayers: camadas inativas ficarão ocultas até se tornarem ativas.

 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
        )
}        

  • nonReversing: a animação não será revertida toda vez que for executada.
  • reversing: toda vez que a animação se repete, ela será revertida.

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!

Entre para ver ou adicionar um comentário

Outros artigos de Gustavo Cosme

Outras pessoas também visualizaram

Conferir tópicos