Animaciones de SFSymbols en SwiftUI
Marcelo Laprea
26 junio, 20235min de lectura
A partir de iOS 17 vamos a poder animar los iconos de SFSymbols que nos provee Apple tal como anunciaron en la WWDC23, usando el modificador symbolEffect(_:options:value:)
. En total son 7 animaciones y 4 comportamientos o behaviors de las animaciones.
Tipos de comportamientos de una animación
- Discrete:
Corresponde al protocolo DiscreteSymbolEffect
, y solo reproduce una animación única al icono.
- Indefinite.
Corresponde al protocolo IndefiniteSymbolEffect
, y reproduce la animación indefinidamente o hasta que se le indique.
- Transition.
Corresponde al protocolo TransitionSymbolEffect
, crea una animación para que el icono aparezca o desaparezca de la vista.
- Content Transition.
Corresponde al protocolo ContentTransitionSymbolEffect
, crea una animación para cambiar un icono por otro.
Animaciones de los iconos
- Bounce.
Su comportamiento es Discrete. Para reproducir esta animación, usamos el modificador .symbolEffect(.bounce, value: value)
indicando .bounce
como animación y un valor que indicará cuando ejecutar la animación. Veamos un ejemplo:
struct ContentView: View {@State var cart: Int = 0var body: some View {VStack(spacing: 13) {Image(systemName: "cart").symbolEffect(.bounce, value: cart).font(.system(size: 30))Button("Añadir producto") {cart += 1}}}}
Básicamente lo que estamos haciendo es animar el icono, cada vez que el valor de nuestro carrito, se ejecute la animación.
- Pulse.
Su comportamiento puede ser tanto Discrete como Indefinite. Para reproducir esta animación, usamos el modificador .symbolEffect(.pulse, isActive: isActive)
indicando .pulse
como animación y también podemos indicar si la animación está activa o no, siendo este parámetro opcional, es decir, si no indicamos isActive
el icono siempre estará animado. Veamos un ejemplo:
struct ContentView: View {var body: some View {VStack(spacing: 18) {Image(systemName: "magnifyingglass").symbolEffect(.pulse).font(.system(size: 30))}}}
En este caso no estamos indicando el valor isActive
por lo que el icono estará animado siempre.
- Variable Color.
Su comportamiento puede ser tanto Discrete como Indefinite. Para reproducir esta animación, usamos el modificador .symbolEffect(.variableColor, isActive: isActive)
indicando .variableColor
como animación y al igual que la animación anterior, podemos indicar si la animación está activa o no, siendo este parámetro opcional. Veamos un ejemplo:
struct ContentView: View {var body: some View {VStack(spacing: 18) {Image(systemName: "shareplay").symbolEffect(.variableColor).font(.system(size: 30))}}}
- Scale.
Su comportamiento es Indefinite. Para reproducir esta animación, usamos el modificador .symbolEffect(.scale, isActive: isActive)
indicando .scale
como animación y al igual que la animación anterior, podemos indicar si la animación está activa o no, siendo este parámetro opcional. Veamos un ejemplo:
struct ContentView: View {@State var isActive = falsevar body: some View {VStack(spacing: 18) {Image(systemName: "trash").symbolEffect(.scale.up, isActive: isActive).font(.system(size: 30))Button("Animar") {isActive.toggle()}}}}
Básicamente le estamos indicando que escale hacia arriba (es decir, que crezca) al pulsar el botón. También podemos indicar .scale.down
si queremos el efecto contrario.
- Appear.
Su comportamiento puede ser tanto Transition como Indefinite. Para reproducir esta animación, usamos el modificador .symbolEffect(.appear, isActive: isActive)
indicando .appear
como animación e indicando si la animación está activa o no. Veamos un ejemplo:
struct ContentView: View {@State var isActive = falsevar body: some View {VStack(spacing: 18) {Image(systemName: "trash").symbolEffect(.appear, isActive: isActive).font(.system(size: 30))Button("Animar") {isActive.toggle()}}}}
Básicamente lo que estamos haciendo al pulsar el botón, es que el icono desaparecerá o aparecerá dependiendo del estado en el que esté, es decir, si el icono ya está presente, este desaparecerá al pulsar el botón, de lo contrario, aparecerá.
- Disappear.
Mismo comportamiento que la animación anterior (Appear), pero esta vez indicando .disappear
. Veamos un ejemplo:
struct ContentView: View {@State var isActive = falsevar body: some View {VStack(spacing: 18) {Image(systemName: "trash").symbolEffect(.disappear, isActive: isActive).font(.system(size: 30))Button("Animar") {isActive.toggle()}}}}
- Replace.
La última animación tiene un comportamiento de Content Transition. Para reproducir esta animación, usamos el modificador .contentTransition(.symbolEffect(.replace))
. Veamos un ejemplo:
struct ContentView: View {@State var isPaused = falsevar body: some View {VStack(spacing: 18) {Button {isPaused.toggle()} label: {Image(systemName: isPaused ? "pause.fill" : "play.fill").contentTransition(.symbolEffect(.replace)).font(.system(size: 30))}}}}
Siguientes pasos con las animaciones de SFSymbols
- Puedes configurar efectos muy específicos sin problemas, como por ejemplo:
variableColor.iterative.hideInactiveLayers
. - Xcode te ayuda a auto-completar cada parte del nombre, y en caso de que tengas algo incorrecto, ta dará un error en tiempo de compilación.
- Recuerda que también puedes usar estos efectos en UIKit, usando la función
addSymbolEffect
.
let imageView: UIImageView = UIImageView(image: UIImage(systemName: "play.fill"))imageView.addSymbolEffect(.variableColor.iterative)
- Puedes quitar los efectos en tu imagen en UIKit usando la función
removeSymbolEffect
.
imageView.removeSymbolEffect(ofType: .variableColor)
- El modificador
symbolEffect
se propaga a través de todas las vistas, veamos un ejemplo:
struct ContentView: View {@State var isActive = truevar body: some View {VStack(spacing: 18) {Image(systemName: "trash").font(.system(size: 30))Image(systemName: "play.fill").font(.system(size: 30))Image(systemName: "pause.fill").font(.system(size: 30))}.symbolEffect(.pulse, isActive: isActive)}}
En este ejemplo, las tres imágenes tendrán la animación pulse
.
- Podemos evitar que una de las imágenes no tenga animación con el siguiente modificador:
symbolEffectsRemoved()
.
Image(systemName: "trash").symbolEffectsRemoved()
- Es posible combinar diferentes efectos:
struct ContentView: View {var body: some View {VStack(spacing: 18) {Image(systemName: "trash").symbolEffect(.variableColor.iterative).symbolEffect(.scale.up).font(.system(size: 30))}}}