← All shorts
13iOS 17+2 min read

symbolEffect: Native Animation for SF Symbols

Bounce, pulse, and morph SF Symbols with built-in effects instead of hand-rolled scale and crossfade tricks.

Reacting to a new notification by nudging a bell icon, or swapping it for a filled variant, used to mean animating generic view modifiers around an Image — SwiftUI had no idea it was looking at a symbol, so it couldn't animate strokes or draw-in transitions.

#The old way

struct NotificationBell: View {
    @State private var notificationCount = 0
    @State private var bump = false

    var body: some View {
        Button {
            notificationCount += 1
            withAnimation(.easeInOut(duration: 0.15)) { bump = true }
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
                withAnimation(.easeInOut(duration: 0.15)) { bump = false }
            }
        } label: {
            Image(systemName: notificationCount > 0 ? "bell.badge.fill" : "bell")
                .font(.system(size: 28))
                .scaleEffect(bump ? 1.2 : 1.0)
        }
    }
}

#The new way

import SwiftUI

struct NotificationBell: View {
    @State private var notificationCount = 0

    var body: some View {
        Button {
            notificationCount += 1
        } label: {
            Image(systemName: notificationCount > 0 ? "bell.badge.fill" : "bell")
                .font(.system(size: 28))
                .symbolEffect(.bounce, value: notificationCount)
                .contentTransition(.symbolEffect(.replace))
        }
    }
}

#Why it matters

  • .symbolEffect(.bounce, value:) fires once, automatically, whenever the observed value changes — no state variable dedicated to tracking "is it bumping right now".
  • .contentTransition(.symbolEffect(.replace)) morphs one SF Symbol into another using the symbol's own paths, instead of a generic crossfade, so multi-layer symbols like bell.badge.fill transition layer by layer.
  • Effects like .pulse and .variableColor come free for things like recording indicators or signal-strength icons, with zero manual timer-driven animation code.

#Gotcha

Not every effect is available on every symbol — .variableColor only works on symbols Apple has annotated for variable rendering, and applying it to one that isn't does nothing silently, no crash, no warning. Separately, indefinite effects like .pulse or .variableColor.iterative keep animating forever once applied — they don't stop on their own. Gate them with the isActive: parameter (.symbolEffect(.pulse, isActive: isRecording)) or you'll burn CPU animating an icon that's scrolled off screen.

swiftuiios17animation

Related shorts