Animating Text in SwiftUI
Asked Answered
P

3

14

SwiftUI has wonderful animation features, but the way it handles changes in Text View content is problematic. It animates the change of the text frame but changes the text immediately without animation. As a result, when the content of a Text View is made longer, animating the transition causes an ellipsis (…) to appear until the text frame reaches its full width. For example, in this little app, pressing the Toggle button switches between shorter and longer text:

Here's the code:

import SwiftUI

struct ContentView: View {
    @State var shortString = true
    var body: some View {
        VStack {
            Text(shortString ? "This is short." : "This is considerably longer.").font(.title)
                .animation(.easeInOut(duration:1.0))
            Button(action: {self.shortString.toggle()}) {
                Text("Toggle").padding()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The question is: how to avoid the ellipsis? When animating a one character string into a two character string the situation is even worse, because the short string is completely replaced by the ellipsis while it animates into the longer string.

One possibility is to assign a separate id to the view in one state or another by adding the modifier, for instance, .id(self.shortString ? 0 : 1) and then adding a .transition() modifier. That will treat the Text as two different Views, before and after. Unfortunately, in my case I need to move text location during the change, and different ids makes animating that impossible.

I guess the solution is a creative use of AnimatableData. Any ideas?

Prowess answered 2/4, 2020 at 0:35 Comment(0)
T
9

Here is a demo of possible approach (scratchy - you can redesign it to extension, modifier, or separate view)

Tested with Xcode 11.4 / iOS 13.4

demo

struct ContentView: View {
    @State var shortString = true
    var body: some View {
        VStack {
            if shortString {
                Text("This is short.").font(.title).fixedSize()
                .transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
            }
            if !shortString {
                Text("This is considerably longer.").font(.title).fixedSize()
                .transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
            }

            Button(action: {self.shortString.toggle()}) {
                Text("Toggle").padding()
            }
        }
    }
}

Any suggestions for shrinking an animated gif's dimensions?

I use this way:
- decrease zoom of Preview to 75% (or resize window of Simulator)
- use QuickTimePlayer region-based Screen Recording
- use https://ezgif.com/video-to-gif for converting to GIF
Tonetic answered 2/4, 2020 at 4:14 Comment(2)
Thank you for the quick and helpful answer, @Asperi! Unfortunately, as it turns out, your solution solves the simplified code I presented but not the general issue I meant it to represent, in which there are many Text views displayed and the values they display can range over a near-infinite number of possibilities rather than just two. Still, it gives me some ideas, so I'll mark it as solved and think it through a bit and tag you if I repost. :)Prowess
Thank you also, @Asperi, for the suggestion to use the simulator for screen capture. I had been using an actual iOS device, which has some advantages, but your way allows easy sizing & cropping and seems better for stackoverflow. :)Prowess
B
6

You can add one by one character into a string with animation after 0.1 seconds additional, but remember to disable the button toggle while the characters being added, like below:demo

Code:

public struct TextAnimation: View {

public init(){ }
@State var text: String = ""
@State var toggle = false

public var body: some View {
  VStack{
    Text(text).animation(.spring())
    HStack {
      Button {
        toggle.toggle()
      } label: {
        Text("Toggle")
      }
    }.padding()
  }.onChange(of: toggle) { toggle in
    if toggle {
      text = ""
      "This is considerably longer.".enumerated().forEach { index, character in
        DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
          text += String(character)
        }
      }
    } else {
      text = "This is short."
    }
  }
}
}
Barbarossa answered 2/12, 2020 at 3:2 Comment(1)
Thank you - the effect is nice. For it to be of general use in SwiftUI I'd want it to work within the Animation framework and to be bidirectional, such that it animated the string length from the length of the prior string to the length of the subsequent string, whether that was longer or shorter.Prowess
U
4

If you add .animation(nil) to the Text object definition then the contents will change directly between values, avoiding ellipsis.

However, this may prevent the animation of the text location, which you also mention wanting to do simultaneously.

Underplay answered 14/9, 2020 at 7:7 Comment(1)
.animation(nil) was deprecated in iOS 15... you should use transaction view modifier instead... should be something like: .transaction { transaction in transaction.animation = .none }Urine

© 2022 - 2024 — McMap. All rights reserved.