Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 62

Respecting Reduced Motion in SwiftUI Animations

Recommended resources

Hello! Welcome to our final lesson in the "SwiftUI Animation Essentials" module.

Throughout this module, we've explored the tools and techniques to build beautiful and engaging animations, from the basics of implicit and explicit animations to the advanced power of the Animatable protocol. Today, we'll address a topic that is fundamental to professional app development: accessibility.

This lesson will focus on how to adapt animations to respect the 'Reduce Motion' accessibility setting in SwiftUI. Creating an inclusive app means providing a comfortable experience for everyone, including users who are sensitive to motion. By the end of this lesson, you will know how to detect this user preference and modify your animations accordingly, ensuring your app is both delightful and accessible.

1. Understanding 'Reduce Motion'

Before we dive into the code, it's crucial to understand what 'Reduce Motion' is and why it's so important. For some individuals, animations involving effects like parallax, scaling, and fast movement can cause discomfort, leading to symptoms like dizziness, nausea, or headaches. This is often referred to as vestibular motion sensitivity.

iOS provides a system-wide setting to mitigate this.

iOS 'Reduce Motion' Accessibility Setting
The 'Reduce Motion' toggle switch as it appears in the iOS Settings app, under Accessibility → Motion.

When a user enables this setting, the operating system reduces or replaces its own animations, for example, by using cross-fades instead of zooming effects for screen transitions. As professional developers, it's our responsibility to ensure our custom animations also respect this choice.

To learn more about the purpose of this setting, please read the introductory section of the following article.

Supporting Reduced Motion accessibility setting in SwiftUI

This article, 'Supporting Reduced Motion accessibility setting in SwiftUI' by Natascha Fadeeva, provides a clear and concise introduction to the 'Reduce Motion' setting.

Read the introduction, which explains why supporting 'Reduce Motion' is important for users with motion sensitivity and where they can find the setting.

Many popular apps do a great job of this. The following article shows GIFs of how the iOS Weather and Telegram apps adapt their UI when 'Reduce Motion' is enabled, providing excellent real-world context.

Reduce Motion: How To Make Your iOS App Animations Accessible ...

This article, 'Reduce Motion: How To Make Your iOS App Animations Accessible...' by Amos Gyamfi, provides great visual examples of 'Reduce Motion' in action.

Read the sections 'What is Reduce Motion?', 'How Does Reduce Motion Work?', and 'Excellent Usage of Reduce Motion on iOS'. Pay attention to the GIFs that demonstrate how the animations change in well-known apps.


2. Detecting 'Reduce Motion' in SwiftUI

SwiftUI makes it straightforward to check if 'Reduce Motion' is enabled through its environment system. You can read this setting using the @Environment property wrapper with the accessibilityReduceMotion key path.

This gives you a boolean value that you can use to conditionally change your view's behavior.

import SwiftUI

struct MyAnimatedView: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    // ... your view body
}

With the reduceMotion property available in your view, you can now decide how your animations should adapt. There are two primary strategies.


3. Strategies for Adapting Animations

Depending on the animation's purpose, you can either disable it completely or replace it with a more subtle alternative.

Strategy 1: Disable Non-Essential Animations

For animations that are purely decorative and don't convey essential information, the best approach is often to disable them entirely. You can do this by conditionally applying a nil animation.

The following reading provides a perfect example of this technique.

Supporting Reduced Motion accessibility setting in SwiftUI

Continuing with Natascha Fadeeva's article, this section shows the practical code for detecting the setting and disabling an animation.

Read the second half of the article, starting from the first code block. Focus on how the @Environment property is used with a ternary operator (? :) to set the animation to nil when reduceMotion is true.

Here's the core pattern from the article:

struct AnimatedLogo: View {
    @State private var isAnimating = false
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    var body: some View {
        Image(systemName: "star.fill")
            .font(.largeTitle)
            .rotationEffect(isAnimating ? .degrees(360) : .zero)
            .animation(
                reduceMotion ? nil : .linear(duration: 1).repeatForever(autoreverses: false),
                value: isAnimating
            )
            .onAppear {
                isAnimating = true
            }
    }
}

In this example, the star rotates continuously by default. However, if reduceMotion is true, the animation modifier receives nil, and the rotation effect is applied instantly without any motion.

Strategy 2: Replace with a Simpler Animation

Sometimes, an animation is integral to communicating a state change. In these cases, disabling it might make the UI feel broken or confusing. The better strategy is to replace a motion-heavy animation (like a spring or scale effect) with a more subtle one, such as a cross-fade (.opacity).

This approach maintains the functional feedback of the animation while removing the potentially jarring motion. The article by Amos Gyamfi provides a clear example of replacing a "bouncy" animation with a gentle easeInOut curve.

Reduce Motion: How To Make Your iOS App Animations Accessible ...

Let's return to the article by Amos Gyamfi to see how to implement the replacement strategy.

Read the sections 'How to Substitute an Excessive Animation With a Subtle One...' and 'How to Disable Animations...'. Focus on the code that uses a ternary operator to switch between a bouncy animation and a subtle one.

The key takeaway is that you have a choice. Your goal is to respect the user's preference while maintaining usability.


4. Practical Application

Let's apply this to an animation we've seen before: the matchedGeometryEffect. This effect creates a smooth size and position animation between two views, but it can be quite motion-intensive.

Here is a simple tab view that uses matchedGeometryEffect to slide an indicator.

struct TabsView: View {
    @State private var selectedTab = "Home"
    @Namespace private var namespace
    
    // Add the environment property here
    
    let tabs = ["Home", "Favorites", "Profile"]

    var body: some View {
        HStack {
            ForEach(tabs, id: \.self) { tab in
                ZStack(alignment: .bottom) {
                    Text(tab)
                        .padding()
                        .onTapGesture {
                            withAnimation(.spring()) {
                                selectedTab = tab
                            }
                        }

                    if selectedTab == tab {
                        // This is a motion-heavy effect
                        Capsule()
                            .fill(Color.blue)
                            .frame(height: 4)
                            .matchedGeometryEffect(id: "indicator", in: namespace)
                    }
                }
            }
        }
    }
}

Your task: Modify the TabsView to respect 'Reduce Motion'.

  1. Add the @Environment property to detect the setting.
  2. When reduceMotion is true, the blue indicator should fade in and out rather than sliding.
  3. When reduceMotion is false, it should continue to use the matchedGeometryEffect.

Hint: You can use an if/else block around the Capsule view. In one branch, use the .matchedGeometryEffect modifier. In the other, use a standard .transition(.opacity) modifier.

Solution
struct TabsView: View {
    @State private var selectedTab = "Home"
    @Namespace private var namespace
    
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    
    let tabs = ["Home", "Favorites", "Profile"]

    var body: some View {
        HStack {
            ForEach(tabs, id: \.self) { tab in
                ZStack(alignment: .bottom) {
                    Text(tab)
                        .padding()
                        .onTapGesture {
                            // You could also replace the spring animation here
                            let animation: Animation = reduceMotion ? .easeIn : .spring()
                            withAnimation(animation) {
                                selectedTab = tab
                            }
                        }

                    if selectedTab == tab {
                        if reduceMotion {
                            // Reduced motion: use a simple fade
                            Capsule()
                                .fill(Color.blue)
                                .frame(height: 4)
                                .transition(.opacity)
                        } else {
                            // Default motion: use matchedGeometryEffect
                            Capsule()
                                .fill(Color.blue)
                                .frame(height: 4)
                                .matchedGeometryEffect(id: "indicator", in: namespace)
                        }
                    }
                }
            }
        }
    }
}

Finally, don't forget to test your implementation. You can enable 'Reduce Motion' in the iOS Simulator by going to Settings > Accessibility > Motion > Reduce Motion.


Conclusion

You've now completed the SwiftUI Animation Essentials module! By learning to adapt your animations for accessibility, you're embracing a core tenet of professional software engineering: building products for everyone.

Key Takeaways:

  • 'Reduce Motion' is an essential accessibility feature for users sensitive to screen motion.
  • You can detect this setting in SwiftUI using @Environment(\.accessibilityReduceMotion).
  • For decorative animations, consider disabling them completely by setting the animation to nil.
  • For functional animations, replace motion-heavy effects with subtler ones, like a cross-fade (.opacity), to maintain clarity while respecting the user's preference.

Preview of the Next Module:

We are now moving from the visual, user-facing aspects of development to the internal logic and robustness of your codebase. The next module, Effective Unit Testing, will equip you with the skills to write reliable and maintainable code. In our first lesson, we'll start with the fundamentals: "Write isolated unit tests for business logic using the Arrange-Act-Assert pattern." This is a critical skill for building professional, high-quality applications.