- Published on
Uncovering and Solving a SwiftUI Text Animation Bug
- Authors

- Name
- Omar Elsayed
Introduction
When working with SwiftUI, developers often encounter unexpected behaviors that can be challenging to debug. Recently, my team in klivvr faced an interesting issue with text view animations that led us down a path of discovery and ultimately revealed a long-standing framework bug.
The Problem
We encountered a situation where text animations weren’t working as expected in SwiftUI. Specifically, the offset animation on Text views wasn’t functioning properly. Here’s what we initially tried:
//To give you more info our app support iOS 15 and above
// Attempt 1: Using traditional animation modifier
Text("Hello, world!")
.offset(y: animate ? 0 : 100) // Doesn't work
.animation(.default, value: animate)
// Attempt 3: Using withAnimation
withAnimation {
// Text animations still failed to work
}
My team memeber Eslam spent considerable time trying different approaches and in the end we discovered this wasn’t just a coding issue — it was actually a known bug in SwiftUI itself.
After searching about the issue we were facing, we found this forum: https://forums.developer.apple.com/forums/thread/748852 .
From it we discovered that this is a well known issue in swiftUI and it was confirmed by Apple engineer in the developer forum thread who acknowledged it by saying :
This is a known bug, thanks so much for filing a bug report!
In the thread title it said that this bug was found in iOS 17 but this isn’t true, this issue was in SwiftUI since 2022 this was proven by this StackOverFlow form: https://stackoverflow.com/questions/74927385/string-inside-text-view-doent-be-animated-i-dont-know-why-it-works-like-that and many more.
The Solution
In Apple Developer forum we found this solution that was developed by someone called kurtlee93. Here’s the working implementation:
public extension View {
func projectionOffset(x: CGFloat = 0, y: CGFloat = 0) -> some View {
self.projectionOffset(.init(x: x, y: y))
}
func projectionOffset(_ translation: CGPoint) -> some View {
modifier(ProjectionOffsetEffect(translation: translation))
}
}
private struct ProjectionOffsetEffect: GeometryEffect {
var translation: CGPoint
var animatableData: CGPoint.AnimatableData {
get { translation.animatableData }
set { translation = .init(x: newValue.first, y: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(CGAffineTransform(translationX: translation.x, y: translation.y))
}
}
You may ask why this works with Text view while it seems like it’s doing the same thing like .offset(), Good question, let me explain it to you.
Dive Deep in to the Solution
Let me break this code down piece by piece:
1. let’s look at the View extension
public extension View {
// This is a convenience method that allows using x and y coordinates separately
func projectionOffset(x: CGFloat = 0, y: CGFloat = 0) -> some View {
self.projectionOffset(.init(x: x, y: y))
}
// This is the main method that applies the custom geometry effect
func projectionOffset(_ translation: CGPoint) -> some View {
modifier(ProjectionOffsetEffect(translation: translation))
}
}
This extension create a nice API where you can use it in different ways like:
view.projectionOffset(y: 20)view.projectionOffset(x: 20)view.projectionOffset(x: 20, y: 20)view.projectionOffset(CGPoint(x: 10, y: 20))
2. Now, the interesting part ProjectionOffsetEffect
private struct ProjectionOffsetEffect: GeometryEffect {
// Stores the current position
var translation: CGPoint
// This is the magic that enables animation
var animatableData: CGPoint.AnimatableData {
get { translation.animatableData }
set { translation = .init(x: newValue.first, y: newValue.second) }
}
// This creates the actual transform that moves the view
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(CGAffineTransform(translationX: translation.x, y: translation.y))
}
}
GeometryEffect, this protocol allows us to create custom geometric transformations that can be animated.animatableData, this varbile is what does the whole magic by:
- Using CGPoint.AnimatableData which is essentially two numbers (x and y)
- The getter converts our CGPoint to animatable data
- The setter updates our position as the animation progresses
effectValue, this creates the actual transform that moves the view by:
- Taking the current size of the view
- Returns a ProjectionTransform which is more powerful than a simple offset
- Uses
CGAffineTransformto create the movement
3. Why this works better than .offset()
- It explicitly defines how the animation should interpolate through
animatableData - It creates a proper geometric transformation instead of using
.offset()which only moves the view without changing the frame position on the other handprojectionOffsetmoves the view with it’s frame.
Practical Takeaways
This experience taught us several valuable lessons:
- When something feels off, it is worth searching and looking if anyone faced a similar thing, this can save a lot of time.
- Solutions often lie in unexpected places (like using projection transforms instead of simple offsets)
- The developer community is an invaluable resource for finding and validating issues
What’s Next?
While our solution works reliably across all iOS versions, it’s worth noting that this is still a workaround. SwiftUI is constantly evolving, and we hope to see this animation issue addressed in future updates.
Until then, feel free to use our custom modifier in your projects. It’s a robust solution that gets the job done.