1

I have a very simple view that only shows a Text, a Shape, and a Button stacked vertically in a ScrollView. The Shape is a Capsule and is conditionally shown only when showCapsule is true.

struct ContentView: View {

    @State var showCapsule = true

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                Text("Why, oh why? ")
                    .font(.headline)
                if showCapsule {
                    Capsule()
                        .frame(maxWidth: .infinity)
                        .frame(height: 100)
                        .foregroundColor(.blue)
                }
                Button {
                    showCapsule.toggle()
                } label: {
                    Text(showCapsule ? "Hide" : "Show")
                }
                .buttonStyle(.bordered)
            }
            .frame(maxWidth: .infinity)
            .padding()
        }
        .animation(.default, value: showCapsule)
    }
}

Observed and expected animation

I want to animate the appearance and disappearance of the Capsule, but the result is totally not what I want. While the Capsule fades out (which is okay), the button is animated in two different ways simultaneously:

  1. Its background shape (the grey rounded rectangle) move from the old to its new position.
  2. Its text fades out at its old position and fades in at its new position.

Observed animation

Of course, (2) is not what I want. Instead, I want the button to move as a unit: The entire thing should move from its old to its new position while the text is faded inside of it.

The broader picture

This is a minimal example for a broader question: How do I animate changes to a view that is semantically the same but value-wise different?

In the Button initializer, I use a ternary operator to conditionally pass a different string to its Text label:

Button {
    showCapsule.toggle()
} label: {
    Text(showCapsule ? "Hide" : "Show")
}

Text("Hide") is a different value than Text("Show"), so I guess that's why SwiftUI can't identify them and doesn't "understand" that it should animate them in place. I observed the same behavior with custom views with let constants. Is there a way to make SwiftUI treat such views – and especially this Button – as a unit and animate them correctly?

Note

I'm not looking for a workaround like using two Text fields (or Buttons) and show/hide them by setting their opacity accordingly. Rather looking for a general solution for this kind of problem that solves the identity problem rather than patching its symptoms.

Mischa
  • 15,816
  • 8
  • 59
  • 117

1 Answers1

3

You can fix this by adding .drawingGroup modifier to your button:

Button(showCapsule ? "Hide" : "Show") {
    showCapsule.toggle()
}
.drawingGroup()
.buttonStyle(.bordered)

enter image description here

Alternatively, you could have two buttons that are shown and hidden:

ZStack {
    Button("Hide") {
        showCapsule.toggle()
    }
    .buttonStyle(.bordered)
    .opacity(showCapsule ? 1 : 0)

    Button("Show in a very long button") {
        showCapsule.toggle()
    }
    .buttonStyle(.bordered)
    .opacity(showCapsule ? 0 : 1)
}

enter image description here

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Works nicely for these specific texts as they have almost the same length, but when you use a longer string for one of the two cases like "Please show me this beautiful capsule!", it still does some weird stuff. (When the longer text is transformed to the shorter text, it extends over the button's bounds and the button's background is cut off, so it's not a rounded rectangle during the transition.) – Mischa Feb 14 '23 at 11:10
  • I've added an option for longer text. – Ashley Mills Feb 14 '23 at 11:46
  • Thanks! But that's using exactly the approach I excluded in the _Note_ in my question. Because if I have a more complex view, not just a button, I would have to completely rebuild that thing, duplicating a lot of code and thereby creating a "second source of truth". There must be some way to not patch the symptoms but fix the underlying identity problem. – Mischa Feb 14 '23 at 12:38
  • You could extract your complex view into it's own View struct, in which case you'd only have to implement it once. – Ashley Mills Feb 14 '23 at 13:35
  • Yes, that's what I mean. But then I would possibly have to pass along multiple arguments and do that twice. (So by complex, I mean more parameters than only a string and an action.) – Mischa Feb 14 '23 at 16:29