SwiftUI animation: Some implicit transition animations broken on iOS 13?

Viewed 3032

Tested on Xcode 11.3.1 and 11.4:

When attaching implicit animations to a transition, some transition types appear to be broken. Specifically, any position-related transitions do not apply the given implicit animation. .slide, .move, .offset are broken. .opacity and .scale seem ok. (See attachment)

Explicit animations seem to work fine in all cases.

Even with custom compound Transitions, the position-related sub-transitions do not respond to implicit animation.

Is this a bug, or expected behavior?

Seems like this would be a problem if you want to trigger multiple different animation curves for specific UI elements based on an implicit property change.

enter image description here


struct MyExample: View {

    @State private var isShowing = true

    private let myAnimation = Animation.spring(response: 0.8, dampingFraction: 0.2, blendDuration: 3.0)
    var body: some View {
        VStack(spacing:20) {
            if self.isShowing {
                Text("Opacity").modifier(MyBigFont())
                    .transition(AnyTransition.opacity.animation(myAnimation))

                Text("Scale").modifier(MyBigFont())
                    .transition(AnyTransition.scale.animation(myAnimation))

                Text("Slide").modifier(MyBigFont())
                    .transition(AnyTransition.slide.animation(myAnimation))

                Text("Move").modifier(MyBigFont())
                    .transition(AnyTransition.move(edge: .trailing).animation(myAnimation))

                Text("Offset").modifier(MyBigFont())
                    .transition(AnyTransition.offset(x: 20, y: 0).animation(myAnimation))

                Text("Custom").modifier(MyBigFont())
                    .transition(AnyTransition.myCustomTransition.animation(myAnimation))
            }

            Spacer()

            Button(action: {
                self.isShowing.toggle()
            }) {
                Text("Implicit Toggle")
            }

            Button(action: {
                withAnimation(self.myAnimation) {
                    self.isShowing.toggle()
                }
            }) {
                Text("Explicit Toggle")
            }
        }
    }

}

struct MyBigFont: ViewModifier {
    func body(content: Content) -> some View {
        content
            .lineLimit(1)
            .padding()
            .background(Color.purple)
            .foregroundColor(.white)
            .cornerRadius(8)
            .font(Font.system(size: 21).bold())
    }
}

struct MyCustomTransition: ViewModifier {

    var isEnabled: Bool

    func body(content: Content) -> some View {

        if isEnabled {
            return content
                .offset(x: 20.0, y: 0.0)
                .opacity(0)
        } else {
            return content
                .offset(x: 0.0, y: 0.0)
                .opacity(1)
        }

    }
}

extension AnyTransition {
    static let myCustomTransition = AnyTransition.modifier(
        active: MyCustomTransition(isEnabled: true),
        identity: MyCustomTransition(isEnabled: false))
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            MyExample()
            Spacer()
        }
    }
}

3 Answers

According to Javier, implicit animations on transitions no longer work as of Xcode 11.2

If anyone has newer information, please reply.

Note that since XCode 11.2, transitions no longer work with implicit animations.

https://swiftui-lab.com/advanced-transitions/

To have implicit transitions animatable correctly it needs to make animatable container that includes these transitions.

Tested with Xcode 11.4 / iOS 13.4.

demo

Here is the only fix made:

struct MyExample: View {

    @State private var isShowing = true

    private let myAnimation = Animation.spring(response: 0.8, dampingFraction: 0.2, blendDuration: 3.0)
    var body: some View {
        VStack(spacing:20) {
            if self.isShowing {

                ...      // all your code here

            }
        }.animation(myAnimation)    // << fix !!
    }
}

Asperi's answer is totally right, just tested it with Xcode 13.2.1 and iOS 15.2 / macOS 12.2 with the following simple example:

struct ContentView: View {
    @State private var show = false
    
    var body: some View {
        VStack(spacing: 20) {
            roundedRect(color: .blue)
            if show {
                roundedRect(color: .red)
                    .transition(.opacity.animation(.easeInOut))
            }
            
            Spacer()
            
            Button(show ? "Hide" : "Show") {
                show.toggle()
            }
        }
        .animation(.easeInOut)
        .padding()
        .frame(width: 190, height: 420, alignment: .top)
    }
}

Suprisingly, this worked:

Opacity transition with implicit animation

Then I tried the same setup, just with the .slide transition, and then it stopped working:

Slide transition with implicit animation

Then I tried moving the animation out to the next container (VStack) like Asperi suggested above:

struct ContentView: View {
    @State private var show = false
    
    var body: some View {
        VStack(spacing: 20) {
            roundedRect(color: .blue)
            if show {
                roundedRect(color: .red)
                    .transition(.slide)
            }
            
            Spacer()
            
            Button(show ? "Hide" : "Show") {
                show.toggle()
            }
        }
        .animation(.easeInOut)
        .padding()
        .frame(width: 190, height: 420, alignment: .top)
    }
}

Lo and behold, the transition started animating again.

Slide transition with implicit animation on the containing VStack

Related