Offset modifier not working when used inside a scoped animation (iOS 17) in SwiftUI

The target view has .opacity() and .offset() modifiers in a scoped animation (iOS 17):

Text("Hello, world!")
.animation(.default) {
$0
.opacity(animate ? 1 : 0.2)
.offset(y: animate ? 0 : 100) // <-- DOESN'T WORK
}

But only the .opacity() works when the state is changed directly or withAnimation{}. The .offset() only works when using withAnimation{}, even though it should animate in both cases, like opacity.

Is this a SwiftUI bug? Did anyone encounter this?

import SwiftUI
struct ContentView: View {
@State private var animate = false
var body: some View {
VStack(spacing: 20) {
Button("Toggle Scoped Animation") {
animate.toggle()
}
Button("Toggle withAnimation{}") {
withAnimation {
animate.toggle()
}
}
Text("Hello, world!")
.animation(.default) {
$0
.opacity(animate ? 1 : 0.2)
.offset(y: animate ? 0 : 100) // <-- DOESN'T WORK
}
}
}
}
#Preview {
ContentView()
}
@main
struct ScopedAnimationOffsetBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

Tested on Xcode 15.3 (15E204a), iOS 17.4 Simulator and iPhone Device.

Bug report FB13693703 filed with Apple.

Answered by Vision Pro Engineer in 783449022

Hi @calin , this is a known bug, thanks so much for filing a bug report! The workaround here is to use:

Text("Hello, world!")
.offset(y: animate ? 0 : 100)
.animation(.default, value: self.animate)

or withAnimation like you have above

Accepted Answer

Hi @calin , this is a known bug, thanks so much for filing a bug report! The workaround here is to use:

Text("Hello, world!")
.offset(y: animate ? 0 : 100)
.animation(.default, value: self.animate)

or withAnimation like you have above

I noted that animation modifier on Text is useless.

This code works the same:

struct ContentView: View {
@State private var animate = false
var body: some View {
VStack(spacing: 20) {
Button("Toggle Scoped Animation") {
animate.toggle()
}
Button("Toggle withAnimation{}") {
withAnimation {
animate.toggle()
}
}
Text("Hello, world!")
// .animation(.default) {
// $0
.opacity(animate ? 1 : 0.2)
.offset(y: animate ? 0 : 100) // <-- DOESN'T WORK
// }
}
}
}

For people who've been experiencing this issue: There is a really stupid but 100% working fix.

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))
}
}

This will works perfectly with scoped animation.

Text("Hello, world!")
.animation(.default) {
$0
.opacity(animate ? 1 : 0.2)
// .offset(y: animate ? 0 : 100) // <-- DOESN'T WORKS!!!
.projectionOffset(y: animate ? 0 : 100) // <-- WORKS!!!
}

I've spent the last 3 days trying to figure out why every other animatable modifier works fine with scoped animation but not offset(). turns out it can. it's just a bug in the implementation, which I don't understand the how, considering how simple the implementation is, but whatever. hope this helps! @calin

Offset modifier not working when used inside a scoped animation (iOS 17) in SwiftUI
 
 
Q