11

Question is simple: how in the world do i get a Text to animate properly?

struct ContentView: View {
    @State var foozle: String = ""

    var body: some View {
        VStack() {
            Spacer()

            Text(self.foozle)
                .frame(maxWidth: .infinity)
                .transition(.opacity)

            Button(action: {
                withAnimation(.easeInOut(duration: 2)) {
                    self.foozle = "uuuuuuuuu"
                }
            }) { Text("ugh") }

            Spacer()
        }.frame(width: 320, height: 240)
    }
}

The problem: the view insists on doing some dumb animation where the text is replaced with the new text, but truncated with ellipses, and it slowly expands widthwise until the entirety of the new text is shown.

Naturally, this is not an animation on opacity. It's not a frame width problem, as I've verified with drawing the borders.

Is this just another dumb bug in SwiftUI that i'm going to have to deal with, and pray that someone fixes it?

EDIT: ok, so thanks to @Mac3n, i got this inspiration, which works correctly, even if it's a little ugly:

Text(self.foozle)
    .frame(maxWidth: .infinity)
    .opacity(op)

Button(action: {
    withAnimation(.easeOut(duration: 0.3)) {
        self.op = 0

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            self.foozle += "omo"

            withAnimation(.easeIn(duration: 0.3)) {
                self.op = 1
            }
        }
    }

}) {
    Text("ugh")
}

3 Answers 3

8

The problem is that SwiftUI sees Text view as the same view. You can use the .id() method on the view to set it. In this case I've just set the value to a hash of the text itself, so if you change the text, the entire view will get replaced.

struct ContentView: View {
    @State var foozle: String = ""

    var body: some View {
        VStack() {
            Spacer()

            Text(self.foozle)
                .id(self.foozle.hashValue)
                .frame(maxWidth: .infinity)
                .transition(.opacity)

            Button(action: {
                withAnimation(.easeInOut(duration: 2)) {
                    self.foozle = "uuuuuuuuu"
                }
            }) { Text("ugh") }

            Spacer()
        }.frame(width: 320, height: 240)
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Oh, that's interesting to know! I'll try this out when I come back to this in a bit, but thanks for the answer!
Much more elegant, and the actual solution to the problem, instead of some hacky workaround. This should be the accepted answer.
5

Transition works when view appeared/disappeared. In your use-case there is no such workflow.

Here is a demo of possible approach to hide/unhide text with opacity animation:

enter image description here

struct DemoTextOpacity: View {
    var foozle: String = "uuuuuuuuu"
    @State private var hidden = true

    var body: some View {
        VStack() {
            Spacer()

            Text(self.foozle)
                .frame(maxWidth: .infinity)
                .opacity(hidden ? 0 : 1)

            Button(action: {
                withAnimation(.easeInOut(duration: 2)) {
                    self.hidden.toggle()
                }
            }) { Text("ugh") }

            Spacer()
        }.frame(width: 320, height: 240)
    }
}

3 Comments

my usecase isn't really to hide/unhide text, rather the value of the Text needs to change, and i want it to change smoothly. that's all. the other answer below attempts to manually set the opacity to 0, change the text, then set it back to 1, but it does not work correctly.
@zhiayang, Opacity as we as Transition are features of View (in your case Text) not of a model which in your case is String. If you animate changing string, SwfitUI animates appearance of string, letter by letter.
right, that makes sense i guess. it's not very intuitive. i found a solution, which i have edited in the question. thanks!
3

If you want to animate on opacity you need to change opacity value on your text element.

code example:

@State private var textValue: String = "Sample Data"
@State private var opacity: Double = 1
    var body: some View {
        VStack{
            Text("\(textValue)")
                .opacity(opacity)
            Button("Next") {
                withAnimation(.easeInOut(duration: 0.5), {
                    self.opacity = 0
                })
                self.textValue = "uuuuuuuuuuuuuuu"
                withAnimation(.easeInOut(duration: 1), {
                    self.opacity = 1
                })
            }
        }
    }

3 Comments

this doesn't quite work; although the new text animates in, the old text simply disappears. the animation closure probably runs in a separate thread, and AFAIK there's no way to chain animations and no completion callback...
Maybe AnimatableModifire can do it. check this link : swiftui-lab.com/swiftui-animations-part3
this gave me the inspiration to reach a solution, so i'll accept this. thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.