Skip to content
charmotion

How to Add Animation to an iOS App (SwiftUI & UIKit)

By the Charmotion team · Updated

Cover illustration for the guide: How to Add Animation to an iOS App (SwiftUI & UIKit), with Charmotion demo characters

To add animation to an iOS app, wrap state changes in SwiftUI's withAnimation for UI feedback, use symbolEffect for icons, and add Lottie or Rive through Swift Package Manager for designer-made art. Use Rive when a character needs states, and AVPlayerLooper when you only need a fixed clip.

Key takeaways

  • Built-in SwiftUI animation covers most UI feedback: withAnimation, .animation(_:value:), and PhaseAnimator/KeyframeAnimator on iOS 17+.
  • symbolEffect (iOS 17+) animates SF Symbols, including custom symbols, with one modifier.
  • Install Lottie from lottie-spm, not the 300 MB+ main repository.
  • Rive's RiveRuntime supports iOS 14+ and lets code switch a character's states live.
  • Read accessibilityReduceMotion and swap loops for a still image when it is on.

Already have a character or logo? Preview your own character animated in a phone splash, a website hero and an empty state — free, in your browser, nothing uploaded.

Which animation approach should you use on iOS?

Start with what is moving and who made it. Most apps need a mix of four approaches:

You want to animateUse
Buttons, layout changes, sheets, togglesSwiftUI withAnimation or UIKit UIView.animate
IconssymbolEffect on SF Symbols (iOS 17+)
A designer-made vector loop: confetti, a spinner, an illustrationLottie
A character that reacts to app eventsRive with a state machine
A pre-rendered clip that never changesVideo with AVPlayerLooper

Built-in APIs cost nothing in app size and follow your state automatically. They are also the most consistent with the rest of iOS: springs, sheet presentations and symbol effects all feel native because they are the same system animations Apple's own apps use. Reach for a third-party runtime only when the artwork comes from a design tool. Lottie plays After Effects exports. Rive plays files from the Rive editor and adds a state machine, so your code can tell a character to switch from idle to loading to success without stitching clips together.

Decision table: built-in APIs for UI feedback, icon effects for icons, a Rive state machine for characters, video for marketing clips
Pick the animation method by job.

How do withAnimation and .animation() work in SwiftUI?

SwiftUI animates the difference between two states. Change a value inside withAnimation and every view that depends on it interpolates to its new position, size or opacity:

struct ExpandingCard: View {
    @State private var expanded = false

    var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .frame(height: expanded ? 240 : 120)
            .onTapGesture {
                withAnimation(.spring()) { expanded.toggle() }
            }
    }
}

The alternative is the .animation(_:value:) modifier, which animates a view whenever the given value changes. Use withAnimation when one action should animate several views, and the modifier when one view should always animate a specific value.

For motion with several steps, iOS 17 added PhaseAnimator and KeyframeAnimator. A phase animator cycles through a sequence of phases:

Image(systemName: "heart.fill")
    .phaseAnimator([false, true]) { content, phase in
        content.scaleEffect(phase ? 1.2 : 1.0)
    }

Use KeyframeAnimator when you need separate timing tracks for properties such as scale, rotation and offset. Both are good for small attention loops, not for full characters.

How do you animate SF Symbols and UIKit views?

On iOS 17 and later, the .symbolEffect modifier animates any SF Symbol. Apple's built-in effects include bounce, pulse, variable color, scale, appear, disappear and replace, and they also work on custom symbols:

Image(systemName: "bell.fill")
    .symbolEffect(.bounce, value: unreadCount)

The bell bounces each time unreadCount changes. This is the cheapest way to give an icon feedback, because there is no asset to ship.

In UIKit, block-based view animation still works and is often all a screen needs:

UIView.animate(withDuration: 0.3) {
    banner.alpha = 1
    banner.transform = .identity
}

Use it for fades, slides and scale changes on existing views. For anything drawn by a designer, such as an illustration or a mascot, hand-coding keyframes in Swift gets slow to change. That is the point where a Lottie or Rive file is easier to maintain: the designer edits the file, and your code only decides when it plays.

How do you add a Lottie animation to an iOS app?

In Xcode, choose File > Add Package Dependencies and enter https://github.com/airbnb/lottie-spm.git. Airbnb recommends this package because it is a small wrapper around prebuilt binaries, while the main lottie-ios repository is over 300 MB. The README lists 4.6.1 as the latest version.

Since 4.3.0, Lottie includes a native SwiftUI LottieView. Add the exported .json to your target and play it:

import SwiftUI
import Lottie

struct ConfettiView: View {
    var body: some View {
        LottieView(animation: .named("confetti"))
            .looping()
            .frame(width: 200, height: 200)
    }
}

The same release added reduced-motion support. Lottie is a good fit for a loading loop, a success burst or an onboarding illustration that plays from start to end. It is less suited to a character that has to change behavior in the middle of a loop based on what the app is doing; that is where Rive's state machines help.

Two practical tips. Keep the exported file small by asking the designer to avoid embedded raster images where vectors work. And test the animation on an older device, since complex After Effects masks and effects can be expensive to render even when the file is small.

How do you add a Rive animation and switch its states?

Add https://github.com/rive-app/rive-ios with Swift Package Manager (or pod 'RiveRuntime'). The Apple runtime supports iOS 14+, tvOS 16+, macOS 13.1+ and visionOS. Drag the .riv into your app target.

With the legacy API, RiveViewModel loads the file, picks a state machine and gives you methods to drive it:

import SwiftUI
import RiveRuntime

struct MascotView: View {
    @StateObject private var mascot = RiveViewModel(
        fileName: "character",
        stateMachineName: "CharacterStateMachine"
    )

    var body: some View {
        mascot.view()
            .frame(width: 220, height: 220)
    }

    func startLoading() { mascot.setInput("mood", value: 5.0) }
    func finished(ok: Bool) {
        mascot.triggerInput(ok ? "success" : "error")
        mascot.setInput("mood", value: 0.0)
    }
}

Keep the view model in @StateObject so it survives redraws. Input names are case-sensitive and must match the file.

Rive's new Apple runtime drops inputs in favor of data binding. You set view model properties instead, for example viewModelInstance.setValue(of: NumberProperty(path: "mood"), to: 5) and viewModelInstance.fire(trigger: TriggerProperty(path: "success")). That only works if the file defines a view model. The Rive in SwiftUI guide covers both runtimes in depth.

How do you loop a video clip in SwiftUI?

If the animation is a fixed, pre-rendered clip, such as a 3D logo reveal, a video is simpler than any runtime. AVPlayerLooper plays an item continuously through an AVQueuePlayer, and SwiftUI's VideoPlayer (AVKit, iOS 14+) displays it:

import SwiftUI
import AVKit

final class LoopingClip: ObservableObject {
    let player = AVQueuePlayer()
    private var looper: AVPlayerLooper?

    init(name: String) {
        guard let url = Bundle.main.url(forResource: name, withExtension: "mp4") else { return }
        looper = AVPlayerLooper(player: player, templateItem: AVPlayerItem(url: url))
        player.isMuted = true
        player.play()
    }
}

struct ClipView: View {
    @StateObject private var clip = LoopingClip(name: "logo-reveal")
    var body: some View { VideoPlayer(player: clip.player) }
}

Keep the looper referenced, or looping stops. A video cannot react to state, and every variation is another file, so use it for clips, not for interactive characters.

VideoPlayer shows playback controls. For a decorative loop without controls, host an AVPlayerLayer in a small UIViewRepresentable instead. If the clip needs a transparent background over your UI, export it as HEVC with alpha in a .mov; the format comparison explains when that beats a GIF or a Rive file.

How do you add an animated mascot to an iOS app?

Treat the mascot as a view of your app's state. List the moments it should react to, give each a state in the file, and map your events to them in one place:

  • Loops for ongoing states: idle, loading, listening, sleeping.
  • Triggers for one-off events: success, error, a greeting on enter.

In practice, keep a small enum such as .idle, .loading and .done(Bool) in your view model and translate it into setInput and triggerInput calls. The state machine handles the transitions, so you never track which clip is on screen.

Four phone screens using a mascot for loading, an empty inbox, a completed payment and a failed upload
A mascot carrying the four most common app states.

If you already have a mascot or logo but no animator, Charmotion turns an uploaded character into 17 animations in about 1–2 hours, including a .riv with a Character view model (a mood number from 0 to 9 and triggers such as success and error) plus SwiftUI code for both the legacy RiveViewModel and the new runtime. See which states a mascot needs for planning the set.

How do you keep iOS animations fast and accessible?

A few rules keep animation from costing battery or excluding users:

  • Animate a handful of views at once, not whole lists. Stop loops when the view leaves the screen.
  • Prefer vectors (SF Symbols, Lottie, Rive) for small UI art and video for photographic or 3D clips.
  • Bundle the animation file instead of downloading it on first launch, so it is ready when the screen appears.

Respect Settings > Accessibility > Motion > Reduce Motion. SwiftUI exposes it as an environment value that updates live:

@Environment(\.accessibilityReduceMotion) private var reduceMotion

var body: some View {
    if reduceMotion {
        Image("mascot-still").resizable().scaledToFit()
    } else {
        mascot.view()
    }
}

When motion carries meaning, such as loading or success, keep it available as text or an accessibility announcement. The reduced motion guide covers the WCAG rules.

Add animation to an iOS app

  1. Start with SwiftUI. Use withAnimation or .animation(_:value:) for UI feedback and symbolEffect for icons.
  2. Add a runtime if needed. Add https://github.com/airbnb/lottie-spm.git for Lottie or https://github.com/rive-app/rive-ios for Rive in Xcode.
  3. Bundle the file. Drag the .json, .riv or .mp4 into the project and confirm it is a member of the app target.
  4. Show it. Use LottieView(animation: .named("name")).looping(), RiveViewModel(fileName:stateMachineName:).view() or VideoPlayer.
  5. Connect it to app state. Call setInput for loops and triggerInput for one-shots when your view model changes.
  6. Respect Reduce Motion. Read accessibilityReduceMotion and show a still image when it is true.

Frequently asked questions

Should I use UIView.animate or withAnimation?

Use withAnimation in SwiftUI views and UIView.animate in UIKit views. Both animate property changes on existing views. SwiftUI's version animates every view that depends on the changed state, so you describe the end state rather than each view's frame. Mixed apps can use both, each in its own layer.

What is the difference between PhaseAnimator and KeyframeAnimator?

Both arrived in iOS 17. PhaseAnimator steps through a list of phases and animates between them, which suits simple pulses and multi-step sequences. KeyframeAnimator gives each property its own timeline of keyframes, which suits precise motion where scale, rotation and position change at different times.

Lottie or Rive for an iOS app?

Use Lottie for linear, designer-made vector loops exported from After Effects. Use Rive when the animation must react to app state, such as a mascot switching between idle, loading and success, because its state machine handles transitions inside the file. Both install with Swift Package Manager and work in SwiftUI.

Does Rive work with UIKit?

Yes. RiveRuntime supports both SwiftUI and UIKit. In the legacy runtime, RiveViewModel can create a UIKit view as well as a SwiftUI view, and the same setInput and triggerInput calls control it. Rive's Apple documentation covers both setups, so you can mix them in one app.

Quick answers from this guide

Sources

Related guides