How to animate SF Symbols in SwiftUI

Improve your app’s UX by animating SF Symbols

Bruno Lorenzo
5 min readSep 19, 2023
Image from Apple

Having a remarkable user experience in our app could be the main factor in user retention.

Animations play a fundamental role in that area. By adding animations to certain actions, we will be providing a more engaging experience to our users.

If we use SF Symbols, we can get pretty cool animations, basically for free. From iOS 17, Swift introduced a new modifier symbolEffect to perform pre-defined animations over SF Symbols.

Here is how we can use this cool feature.

We have seven built-in animations available to apply to any SF Symbol: Bounce, Pulse, Variable Color, Scale, Appear, Disappear, Replace

All animations are grouped into four types of behaviors

  1. Discrete: the animation runs one time and ends.
  2. Indefinite: the animation will continue until we remove it or disable it.
  3. Transition: animate a symbol in or out of the view.
  4. Content Transition: for animating a replacement of one symbol with another.

Discrete

We need to use symbolEffect(_:options:value:) function wherevalue could be any Equatable type. The effect will be applied every timevalue changes.

struct DiscreteDemoAnimationsView: View {

@State private var animationCount = 0

var body: some View {
VStack {
VStack {
Text("Bounce")
Image(systemName: "wifi.router")
.symbolEffect(
.bounce,
value: animationCount
)
}

VStack {
Text("Pulse")
Image(systemName: "wifi.router")
.symbolEffect(
.pulse,
value: animationCount
)
}

VStack {
Text("Variable Color")
Image(systemName: "wifi.router")
.symbolEffect(
.variableColor,
value: animationCount
)
}

Button("Animate") {
animationCount += 1
}
}
}
}

Indefinite

We control when the animation starts and when the animation stops using symbolEffect(_:options:isActive:).

struct IndefiniteAnimationsView: View {

@State private var animationIsActive = false
private var buttonTitle: String {
return animationIsActive ? "Stop animations" : "Start animations"
}

var body: some View {
VStack {
VStack {
Text("Pulse")
Image(systemName: "wifi.router")
.symbolEffect(
.pulse,
isActive: animationIsActive
)
}

VStack {
Text("Variable Color")
Image(systemName: "wifi.router")
.symbolEffect(
.variableColor,
isActive: animationIsActive
)
}

VStack {
Text("Scale")
Image(systemName: "wifi.router")
.symbolEffect(
.scale.up,
isActive: animationIsActive
)
}

VStack {
Text("Appear")
Image(systemName: "wifi.router")
.symbolEffect(
.appear,
isActive: !animationIsActive
)
}

VStack {
Text("Disappear")
Image(systemName: "wifi.router")
.symbolEffect(
.disappear,
isActive: animationIsActive
)
}

Button(buttonTitle) {
animationIsActive.toggle()
}
}
}
}

Transition

The difference between Appear/Disappear with Discrete behavior is that in a Transition behavior, the symbol is actually removed from the View. So we need to handle the rendering ourselves.

We need to use .transition modifier together with symbolEffect.

struct TransitionAnimationsView: View {
@State private var symbolIsHidden = true
private var buttonTitle: String {
return symbolIsHidden ? "Show" : "Hide"
}

var body: some View {
VStack {
if !symbolIsHidden {
Image(systemName: "wifi.router")
.transition(.symbolEffect(.appear))
}

Button(buttonTitle) {
symbolIsHidden.toggle()
}
}
}
}

Content Transition

To animate a replacement, we must use contentTransition modifier together with symbolEffect.

struct ContentTransitionAnimationsView: View {
@State private var muteOn = false
private var buttonTitle: String {
return muteOn ? "Unmute" : "Mute"
}

var body: some View {
VStack {
Button {
muteOn.toggle()
} label: {
Label(buttonTitle, systemImage: !muteOn ? "speaker.wave.3.fill" : "speaker.slash.fill")
}
.contentTransition(.symbolEffect(.replace))
}
.font(.largeTitle)
}
}

Customization

For both Discrete and Indefinite behavior, we have available the following options to use:

  1. repeating: the effect will be repeated indefinitely.
  2. nonRepeating: the effect will execute once.
  3. repeat(_:): it's up to us to pick how many times we want the effect to be repeated
  4. speed(_:): to manipulate the speed of the effect

In addition, every effect has a set of options that we can use to customize a little bit more the animation

Bounce, Scale, Pulse*, Appear, and Disappear

  1. up & down: to control the effect direction
  2. byLayer & wholeSymbol: to control the effect scope. This applies to the symbols that have more than one layer (like WiFi, Speaker, etc.)

* For Pulse effect, only option 1 is available

Variable Color

For multi-layer symbols

  1. cumulative: each layer will be animated in sequence, and each layer will remain until the animation is complete.
  2. iterative: animate one layer at a time and disable the layer until the animation is done.
  3. nonReversing: the animation won't reverse every time it executes.
  4. reversing: every time the animation repeats, it will reverse.
  5. dimInactiveLayers: inactive layers will be dimmed until they become active.
  6. hideInactiveLayers: inactive layers will be hidden until they become active.

Replace

  1. downUp, offUp & upUp: to control the direction of the effect
  2. byLayer & wholeSymbol: same as for bounce, scale, appear, and disappear

How can we use it?

Just chain the options that we want together with the effect.

Image(systemName: "tv.badge.wifi")
.symbolEffect(
.variableColor.cumulative.dimInactiveLayers.reversing,
options: .speed(1.5).repeat(2),
value: animationCount
)

Bonus

SF Symbols 5 App

The best way to test different combinations is using SF Symbols App. We can even copy the configuration code.

Demo App

I've put together a simple demo app showcasing a bunch of different configurations you can use.

If you're interested, here is the code.

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on Twitter for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo
Bruno Lorenzo

Written by Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy

Responses (1)