You've probably seen this scenario before: a developer falls in love with a concept from Dribbble, spends two days implementing the most complex transition chain, and then wonders why their iPhone 15 Pro starts heating up after five minutes of app usage. Animation is always a deal with the devil (hardware). The question is just how favorable an exchange rate you can negotiate.
The Price of Every Frame: CPU, GPU, and Battery
When we launch an animation, we force the system to work at its limit for a short period of time. In an ideal world (60 or 120 FPS), we have about 8–16 milliseconds to prepare a frame.
- CPU (Layout & Preparation): This is where the most "expensive" stuff happens. Recalculating constraints, creating view hierarchies, calculating paths - all of this falls on the central processor's shoulders. If you animate
frame or bounds on a view with tons of child elements, expect trouble: the system will call layoutSubviews() at every step.
- GPU (Rendering): The graphics chip takes care of rasterization and layer compositing. The main enemies here are transparency (Alpha Blend), blur (Visual Effects), and shadows without a specified
shadowPath.
- Battery: This is the "invisible" metric. High CPU/GPU Usage directly converts into percentage points of charge. Users might not notice a drop to 55 FPS, but they'll definitely notice that your messenger "ate" 20% of their battery in half an hour.
Implicit Animations: The Magic of withTransaction
In SwiftUI, we're used to .animation(.easeInOut, value: target). But sometimes we need finer control over the process, especially when we want to synchronize changes or suppress animation for specific elements.
I prefer using withTransaction for complex transitions. This allows you not just to "turn on" animation, but to pass context: for example, disable animation for a specific change inside the block while preserving it for everything else.
var transaction = Transaction(animation: .spring())
transaction.disablesAnimations = false // Flexible control over the logic
withTransaction(transaction) {
self.showDetails.toggle()
// This change will pick up the transaction settings
}
This is much cleaner than trying to wrap every view with its own .animation(). Plus, it helps avoid the "jello effect," when elements fly off in different directions due to conflicting instructions.
UIView.animate vs SwiftUI Animation
If you're still maintaining UIKit projects (or writing wrappers for SwiftUI), the question arises: what to use?
UIView.animate is good old-fashioned classic, working on top of Core Animation. It's predictable. But in SwiftUI, animations are built on a different principle: they depend on state changes (State-driven).
My rule is simple:
- If you need to animate
CALayer properties (for example, borderWidth or specific masks) - go with UIKit/Core Animation.
- If you're working with navigation and content updates in SwiftUI - use native tools.
The main mistake in SwiftUI is animating everything with .animation(.default). Always specify the concrete value (value) that the animation should react to. Otherwise, you risk getting parasitic movements when other screen properties update.
The Cardinal Sin: Layout on Main Thread
The fastest way to kill performance is to force the Main Thread to calculate geometry during animation. When you change constraints inside an animation block, you trigger a heavy recalculation process for the entire tree.
How to do it right: Instead of changing height or width, use CGAffineTransform (in UIKit) or .scaleEffect() / .offset() (in SwiftUI). These properties are handled by the GPU. The system simply takes an already prepared "snapshot" of the view and transforms it without recalculating the positions of neighboring elements.
// Bad: triggers Layout recalculation
view.frame.size.height = 200
// Good: just a visual transformation
view.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
Xcode Instruments: The Developer's Eyes
If animation "lags" to the naked eye - you've already lost. But how do you understand why it lags? I run Animation Hitches in Xcode Instruments at least once a week.
- Core Animation Instrument: Shows frame rate and, more importantly, "Color Offscreen-Rendered Yellow". If your screen is flooded with yellow - you're forcing the GPU to do double work (for example, rendering masks or shadows without optimization).
- Hitch Rate: Apple introduced this term to measure interface "stuttering". A good metric is less than 5 ms/s.
Accessibility: Respect Your User
Remember people who get motion sick from abrupt movements. iOS has a Reduce Motion setting.
Ignoring it is a sign of poor taste and unprofessionalism. If a user asked the system not to move, your full-screen zoom effect should turn into a simple and elegant crossfade.
In SwiftUI, this is done elementarily:
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
Circle()
.offset(x: moveIt ? 100 : 0)
.animation(reduceMotion ? nil : .easeInOut, value: moveIt)
}
I always check this flag. Trust me, users with vestibular disorders will thank you (even though you'll never know about it).
The "Sufficiency" Guideline
Polish is cool, but it's easy to turn an app into a Christmas tree. Here are my internal criteria for "healthy" animation:
- Duration: Most UI animations should fit within the 0.2 – 0.3 second range. Anything longer starts to irritate with frequent use.
- Functionality: Animation should explain where an element came from and where it went. If it's just "for beauty" and doesn't carry meaning - delete it.
- Responsiveness: If a user tapped a button, the reaction should be instant. Don't make them wait for an animation to finish to perform the next action (Interruptible animations are everything).
Animation is a balance between art and mathematics. Try right now to open your most complex screen in Xcode Instruments and look at the Hitch Rate. If you see red zones - start by replacing constraint animations with CGAffineTransform.