SwiftUI made animations more declarative and concise than UIKit, but the "why isn't this working" rabbit hole is just as deep as ever. Stacking .animation() modifiers, tweaking the scope of withAnimation, watching the preview fail in confusing ways — it still eats hours if you approach it without a clear mental model.
I've been building iOS apps independently since 2013. Beautiful HD Wallpapers, one of the apps I've maintained over those years, has crossed 50 million downloads. Wallpaper apps live or die on the quality of their scroll, zoom, and transition animations — so animation code is something I revisit regularly. Over the past year, the way I use Claude Code for this work has shifted in a few meaningful ways. Here's what's actually helped.
Why "Just Fix This" Doesn't Work Well for Animations
Dropping a Swift file into Claude Code and asking it to "improve the animation" rarely produces useful output on the first try. Animation problems are almost always an intent problem before they're a code problem. The code might be technically valid and still be completely wrong for what you're trying to communicate to the user.
Take a card-flip animation as a concrete example. If I hand Claude Code my Card.swift and say "improve the animation," it has no way to know which dimension of "improve" matters here:
- Is the duration wrong?
- Does the easing feel unnatural at the start?
- Does the back face flicker during the rotation?
- Is the timing relationship between the flip and a secondary label animation off?
Claude Code can't do that disambiguation for me. I need to bring that information to the conversation, and the quality of what comes back is almost entirely determined by the quality of what I bring.
This sounds obvious written out, but in practice there's a strong pull toward just pasting the code and hoping. The three-element structure below is what I use to resist that pull.
The Three-Element Prompt Pattern
After enough iterations, I settled on a consistent structure for animation-related prompts:
[Current] Tapping a card triggers .rotation3DEffect. Duration is 0.3s, linear.
[Expected] Fast start, eases toward the end. A brief pause after the back face appears.
[Constraint] iOS 16+, Xcode 16.x. No side effects on sibling views.
The three elements are: current state, expected behavior, and constraints.
The constraint field is what makes this structure particularly effective. Specifying the minimum iOS version filters out suggestions that would require availability guards for APIs you can't use. Specifying scope — "no side effects on sibling views" — prevents Claude Code from suggesting refactors that, while arguably cleaner, would require touching code you don't want to touch right now.
Expected behavior is where most of the value lives, though. "Feels more natural" is too vague to act on. "Fast start, eases toward the end, brief pause after back face appears" gives Claude Code something specific to optimize toward.
I didn't think in terms of structured prompts when I started building apps independently. Working with Claude Code made me notice that I'd been asking imprecise questions and blaming the tool for imprecise answers. The three-element pattern is essentially just a discipline for asking better questions.
Animation Patterns from the Wallpaper App
Paging Transition Between Wallpapers
In Beautiful HD Wallpapers, switching between wallpapers uses a custom paging transition rather than TabView's default behavior. The default cut felt abrupt — users described it as "jarring," and that description was accurate. There was no visual momentum communicating that the content was being replaced, not just updated.
The condensed prompt I used:
[Current] TabView with .page style. The cut between wallpapers feels abrupt on swipe.
[Expected] Current wallpaper slides left out of frame. Next wallpaper fades in from the right.
easeInOut curve, approximately 0.35s.
[Constraint] iOS 16+. Wallpapers load via AsyncImage — placeholder visible while loading.
The transition should work correctly even if the next image isn't loaded yet.
The core of what Claude Code suggested, after cleanup:
struct PageTransitionModifier: ViewModifier {
let offset: CGFloat
let opacity: Double
func body(content: Content) -> some View {
content
.offset(x: offset)
.opacity(opacity)
.animation(.easeInOut(duration: 0.35), value: offset)
}
}
// Call site
WallpaperView(wallpaper: current)
.modifier(PageTransitionModifier(
offset: isTransitioning ? -UIScreen.main.bounds.width : 0,
opacity: isTransitioning ? 0 : 1
))The AsyncImage constraint was the tricky part — the transition needed to feel smooth regardless of whether the next wallpaper had finished loading. Claude Code's initial suggestion handled the loaded state cleanly but treated the placeholder as an afterthought. The final implementation required two or three rounds of refinement.
That's typical. Claude Code rarely returns production-ready animation code on the first pass. What it consistently does is provide a reasonable starting shape — something to refine rather than something to figure out from scratch — which is where most of the time savings come from.
Fine-Tuning a Tap Response Animation
Another case from Beautiful HD Wallpapers: tapping a wallpaper thumbnail to open a detail view. The thumbnail needed to feel like it was being "selected" — a subtle scale-up to confirm the tap before the transition plays.
The first version used a simple .spring() without parameters. It worked, but the spring felt slightly overdamped — a small bounce at the end that I wanted to eliminate.
[Current] .spring() modifier on scaleEffect. Slight bounce at the end feels off.
[Expected] Scale from 1.0 to 1.05 quickly, no bounce, hold briefly, then transition.
Should feel like physical pressure, not a trampoline.
[Constraint] iOS 16+. Transition to detail view follows immediately — timing needs to coordinate.
The key adjustment Claude Code suggested was using spring(response:dampingFraction:) with a high damping fraction instead of the parameterless .spring():
withAnimation(.spring(response: 0.2, dampingFraction: 0.9)) {
isSelected = true
}A dampingFraction close to 1.0 eliminates the oscillation. The response parameter controls how quickly it reaches the target. This is the kind of parameter relationship that's documented but easy to forget in practice — having it surfaced by Claude Code during a review saved me the trip to the documentation.
Using Claude Code as a Reviewer
One of the more useful patterns is treating Claude Code as a code reviewer rather than a code generator. Hand it existing code and ask for critique instead of asking it to write from scratch. This surfaces issues that accumulate when you've been inside a codebase long enough to stop noticing them.
Before:
Button(action: { showDetail = true }) {
Image(uiImage: wallpaper.thumbnail)
.resizable()
.scaledToFill()
}
.animation(.spring(), value: showDetail)I asked: "Point out any animation-related issues in this code."
Claude Code flagged three things:
- Attaching
.animation()to theButtonpropagates it to all child views. Any view inside the button that depends onshowDetailwill animate, whether or not that's the intent. - The animated property isn't explicit — it's not clear which visual attribute is supposed to change when
showDetailbecomes true. .spring()without parameters has version-dependent defaults. Explicit parameters are more predictable across iOS versions.
After:
Button(action: {
withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) {
showDetail = true
}
}) {
Image(uiImage: wallpaper.thumbnail)
.resizable()
.scaledToFill()
.scaleEffect(showDetail ? 1.05 : 1.0)
}The scaleEffect makes the animated property explicit. The withAnimation block scopes the animation to the state change, not the view hierarchy.
As an indie developer maintaining multiple apps without a team, getting targeted feedback like this isn't something that happens naturally. Code review is a collaboration I mostly do with Claude Code now, and for animation issues specifically, it's been reliable enough that I use it consistently before any change goes into a release branch.
First-Pass Performance Diagnosis
High-resolution image apps put pressure on memory and the rendering pipeline. When animation performance degrades on older devices — frame drops, stuttering during transitions — the cause is often not the animation itself but something nearby: image decoding happening synchronously on the main thread, heavy operations inside onAppear, or excessive view invalidation on each animation frame.
Before opening Instruments, I pass the suspicious section to Claude Code with this kind of request: "Identify patterns in this code that are likely to cause animation performance problems on older devices."
It won't catch everything. Instruments is still the right tool for confirming and measuring. But as a first pass — something to do before the 15-minute setup of a proper profiling session — Claude Code consistently identifies the most common culprits. It's narrowed my Instruments sessions from "find the problem" to "confirm the problem I already suspect."
Where to Start
If there's animation code in your project that's been "good enough for now" for a while — timing that never quite felt right, a transition that's slightly off but not broken enough to prioritize — pick one instance and write the three elements: current state, expected behavior, constraints.
The act of writing those three elements often clarifies what the problem actually is before Claude Code even responds. And the habit of writing them transfers well beyond AI tools: PR descriptions, inline comments, technical conversations — structured intent makes all of those better too.