Movimento reduzido (Reduce Motion) no SwiftUI e UIKit
Por equipe Charmotion · Atualizado em

No SwiftUI, leia @Environment(\.accessibilityReduceMotion) var reduceMotion; o valor é atualizado na hora quando o usuário muda o ajuste. No UIKit, verifique UIAccessibility.isReduceMotionEnabled e observe UIAccessibility.reduceMotionStatusDidChangeNotification. Quando estiver ativado, substitua loops ou movimentos grandes por uma pose parada ou um crossfade, e pause as views de Rive e Lottie.
Principais pontos
accessibilityReduceMotionestá disponível a partir do iOS 13, watchOS 6 e visionOS 1;isReduceMotionEnableda partir do iOS 8.- O SwiftUI renderiza de novo automaticamente quando o ajuste muda; o UIKit precisa da notificação de mudança.
- O novo runtime do Rive tem
.paused(_:)noRiveUIViewRepresentableeisPausednoRiveUIView. - Mantenha as mudanças de estado e tire o movimento: o mascote ainda deve mostrar "success" como uma pose, não desaparecer.
- Pule totalmente intros decorativas e comemorações que tocam uma vez.
Como ler o Reduzir Movimento no SwiftUI?
Declare o valor de ambiente em qualquer view. Ele é true quando Ajustes → Acessibilidade → Movimento → Reduzir Movimento está ativado, e o SwiftUI reavalia o body quando ele muda.
struct PulsingBadge: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var saved = 0
var body: some View {
Image(systemName: "checkmark.seal.fill")
.font(.largeTitle)
.phaseAnimator([false, true], trigger: saved) { content, lifted in
content.scaleEffect(lifted && !reduceMotion ? 1.2 : 1)
}
.onTapGesture { saved += 1 }
}
}
Para animações explícitas, escolha uma animação mais suave ou nenhuma, em vez de ramificar a view inteira:
withAnimation(reduceMotion ? nil : .spring(duration: 0.4, bounce: 0.3)) {
isExpanded.toggle()
}
// Or swap movement for a fade
.transition(reduceMotion ? .opacity : .move(edge: .bottom))
Os princípios mais amplos (o que conta como movimento, WCAG 2.3.3) estão em animação acessível e movimento reduzido; este guia foca em código para plataformas Apple.
Como ler o Reduzir Movimento no UIKit e acompanhar as mudanças?
UIAccessibility.isReduceMotionEnabled retorna o valor atual. Ele não avisa você sobre mudanças, então observe UIAccessibility.reduceMotionStatusDidChangeNotification para atualizar uma tela que já está visível:
import UIKit
import RiveRuntime
final class MascotViewController: UIViewController {
private let riveView = RiveUIView(rive: nil)
private let stillView = UIImageView(image: UIImage(named: "mascot-idle"))
private var motionObserver: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
// ... add riveView and stillView in the same frame, load the Rive file ...
applyMotionPreference()
motionObserver = NotificationCenter.default.addObserver(
forName: UIAccessibility.reduceMotionStatusDidChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in self?.applyMotionPreference() }
}
}
deinit {
if let motionObserver { NotificationCenter.default.removeObserver(motionObserver) }
}
private func applyMotionPreference() {
let reduce = UIAccessibility.isReduceMotionEnabled
riveView.isPaused = reduce
riveView.isHidden = reduce
stillView.isHidden = !reduce
}
}
Em chamadas de UIView.animate, verifique a flag no momento da chamada e use um cross-dissolve mais curto, ou defina o estado final sem animação.
O que um mascote deve fazer quando o Reduzir Movimento está ativado?
Remova o movimento, mas mantenha o significado. Um mascote que informa estado ("carregando", "pronto", "algo deu errado") deve continuar informando.
| Movimento completo | Alternativa reduzida |
|---|---|
| Loop idle de respiração ou piscada | Pose idle estática |
| Loop de carregamento | Pose "thinking" estática com um texto ou indicador de progresso |
| Reação única de success / error | Troca para uma imagem parada happy ou sad com um fade curto, mais um rótulo de texto |
| Animação de entrada (entra voando, quica) | Entrada com crossfade |
| Intro de abertura | Pule, ou mostre um frame parado durante o carregamento |
| Loop de attention para um item novo | Pose attention estática e um badge |
O jeito mais fácil de implementar isso é um dicionário de mood para imagem parada, usando frames do mesmo personagem para que a versão reduzida pareça intencional:
struct MascotView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@ObservedObject var character: CharacterModel // Rive controller that keeps the view model instance
let mood: Int
var body: some View {
ZStack {
if reduceMotion {
Image("mascot-mood-\(mood)")
.resizable()
.scaledToFit()
.id(mood)
.transition(.opacity)
} else {
RiveUIViewRepresentable(rive: character.rive)
}
}
.animation(.easeInOut(duration: 0.2), value: mood)
.onChange(of: mood) { _, newMood in character.setMood(newMood) }
}
}
Como pausar Rive ou Lottie com o Reduzir Movimento?
Pausar congela o frame atual, o que é aceitável para um loop decorativo se esse frame funcionar como pose. Cada runtime tem uma chave direta:
// Rive, new Apple runtime (SwiftUI)
RiveUIViewRepresentable(rive: character.rive)
.paused(reduceMotion)
// Rive, new runtime (UIKit)
riveView.isPaused = UIAccessibility.isReduceMotionEnabled
// Rive, legacy runtime
reduceMotion ? riveViewModel.pause() : riveViewModel.play()
// Lottie
LottieView(animation: .named("idle"))
.playbackMode(reduceMotion
? .paused(at: .progress(0))
: .playing(.fromProgress(0, toProgress: 1, loopMode: .loop)))
Dois cuidados:
- Um frame pausado pode estar no meio do movimento. Se você pausar uma view Rive depois que ela já estava tocando, ela pode parar em uma posição intermediária estranha. Definir o mood para um estado calmo (idle) antes de pausar, ou mostrar uma imagem parada no lugar, evita isso.
- Não dispare triggers com a animação pausada. Uma state machine pausada não avança, então as reações únicas não vão tocar. Mostre o resultado na imagem parada ou em texto.
O Lottie 4.3.0 e versões posteriores também podem tocar automaticamente um marcador chamado reduced motion quando o Reduzir Movimento está ativado, se o designer incluir um no arquivo.
Ainda devo mostrar a intro de abertura com o Reduzir Movimento?
Não. Uma intro é decorativa por definição, e pulá-la é o ganho mais claro para quem ativou o ajuste. Condicione a sobreposição ao valor de ambiente:
struct RootView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var showIntro = true
var body: some View {
ZStack {
ContentView()
if showIntro && !reduceMotion {
IntroView { withAnimation { showIntro = false } }
.transition(.opacity)
.zIndex(1)
}
}
}
}
O padrão completo, incluindo como combinar com a launch screen estática, está em splash screen animada em SwiftUI.
Se o personagem tem significado no seu app, as poses estáticas podem vir direto do conjunto de animações: em todos os planos pagos, cada conjunto inclui um pôster estático de cada animação ao lado do .riv, e exporta as 17 como MP4, GIF e WebM/MOV transparentes sob demanda, assim você tem uma imagem estática combinando com cada humor.
Como aplicar o Reduzir Movimento a transições e efeitos de rolagem?
A animação do personagem é só uma parte do movimento em um app. As transições fornecidas pelo sistema ficam por conta da plataforma; cada efeito personalizado que você adiciona, cabe a você adaptar.
- Transições de inserção e remoção: troque
.move,.slidee.scalepor.opacity. - Efeitos guiados pela rolagem (
scrollTransition, cabeçalhos com parallax): retorne o efeito identidade quando o movimento estiver reduzido. - Animações hero com
matchedGeometryEffect: mantenha a mudança de estado, mas remova a animação passandonilparawithAnimation. - Efeitos de símbolos: prefira efeitos sem deslocamento, como uma mudança de cor, em vez de quiques.
struct CardRow: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
let title: String
var body: some View {
Text(title)
.padding()
.scrollTransition { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0.6)
.scaleEffect(reduceMotion || phase.isIdentity ? 1 : 0.92)
}
}
}
Um pequeno helper mantém a escolha consistente em todo o app:
extension Animation {
static func motionAware(_ animation: Animation, reduceMotion: Bool) -> Animation? {
reduceMotion ? nil : animation
}
}
withAnimation(Animation.motionAware(.bouncy, reduceMotion: reduceMotion)) { selected = item }Como testar o Reduzir Movimento rapidamente?
Três formas, da mais rápida para a mais lenta:
- Previews do SwiftUI:
accessibilityReduceMotioné somente leitura, então.environment(\.accessibilityReduceMotion, true)não compila. Leia o valor em uma view pai e passe um parâmetroreduceMotion: Boolpara a view que você quer visualizar; depois, mostre os dois valores lado a lado no preview. - Accessibility Inspector: Xcode → Open Developer Tool → Accessibility Inspector, selecione o simulador e ative ou desative o Reduce Motion no painel de configurações enquanto o app roda.
- Ajustes do simulador ou do dispositivo: Ajustes → Acessibilidade → Movimento → Reduzir Movimento.
Confira cada tela nos dois estados: se os loops param, se nada importante desaparece e se alterar o ajuste com a tela aberta a atualiza (o SwiftUI faz isso automaticamente; o UIKit só se você observar a notificação).
Perguntas frequentes
O accessibilityReduceMotion está disponível no watchOS e no visionOS?
Sim. A Apple indica o valor de ambiente accessibilityReduceMotion do SwiftUI para iOS 13 ou posterior, watchOS 6 ou posterior e visionOS 1. No UIKit, UIAccessibility.isReduceMotionEnabled está disponível desde o iOS 8, e a reduceMotionStatusDidChangeNotification avisa quando o usuário altera o ajuste com o app em execução.
O Reduzir Movimento deve parar toda a animação?
Não. Ele pede que você reduza o movimento, principalmente deslocamentos grandes, em loop ou com parallax. Fades curtos, mudanças de cor e trocas de estado geralmente não são problema. Mantenha o feedback que mostra ao usuário que algo aconteceu, só que sem deslocamento, mudança de escala ou quiques, para que ninguém perca informação por ter ativado o ajuste.
O Rive respeita o Reduzir Movimento automaticamente?
Não. O runtime do Rive reproduz o que a state machine mandar. Leia você mesmo accessibilityReduceMotion ou UIAccessibility.isReduceMotionEnabled e pause a view com .paused(_:) ou isPaused, ou substitua-a por uma imagem estática do humor atual. Defina um humor calmo antes de pausar, para que o frame congelado pareça uma pose.
Respostas rápidas deste guia
Fontes
- accessibilityReduceMotion, Apple
- UIAccessibility.isReduceMotionEnabled, Apple
- UIAccessibility.reduceMotionStatusDidChangeNotification, Apple
- Apple runtime (new and legacy), Rive
- RiveUIViewRepresentable.swift, Rive (GitHub)
- lottie-ios 4.3.0 release notes, Airbnb (GitHub)