Search code examples
iosswiftswiftuiswiftui-animation

How to animate substring in a Text?


Currently, I am using multiple Texts in a horizontal stackview, to achieve animation of substring.

enter image description here

As you can see in the above animation, the text

- conversation
- meeting
- lecture

are animated.

However, there shortcoming of such an approach.

Text size is not consistent among different Text block. The following Text block are having different text size.

- Transform
- conversation/ meeting/ lecture
- to Quick Note

Any idea how we can achieve, so that all text blocks have same text size so that they appear like 1 sentence?

Or, how we can make the text blocks having constant text size, but able to perform line wrapping to next line, so that they appear like 1 sentence?

Currently, this is the code snippet I am using.

import SwiftUI

struct ContentView: View {
    var array = ["lecture", "conversation", "meeting"]
    
    @State var currentIndex : Int = 0
    @State var firstString : String = ""

    var body: some View {

        VStack {
            HStack {
                Text("Transform")
                    .lineLimit(1)
                    .minimumScaleFactor(0.5)
                    .font(.title)
                
               Text(firstString)
                    .lineLimit(1)
                    .minimumScaleFactor(0.5)
                    .font(.title)
                    .transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
                    .background(.yellow)
                
                Text("to Quick Note")
                    .lineLimit(1)
                    .minimumScaleFactor(0.5)
                    .font(.title)
            }.padding()
        }
        .animation(.default)
        .onAppear {
            firstString = array[0]
            
            let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
                if currentIndex == array.count - 1 {
                    self.firstString = array[0]
                    currentIndex = 0
                }
                else {
                    self.firstString = array[currentIndex+1]
                    currentIndex += 1
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

Solution

  • To keep it all on one line with a consistent font size, try these changes:

    • Move the modifier .minimumScaleFactor to the HStack.
    • Apply .scaledToFit to the HStack too.

    Other suggestions:

    • You only really need one state variable, not two.
    • It is simpler to apply modifiers like .font to the HStack, instead of to each individual Text item.
    • The .transition modifier is redundant, because it is the content of the Text that is changing, no views are being inserted or removed. However, you could consider using .contentTransition if you don't want the default .opacity transition to be seen when the content changes.
    • The way you are using the .animation modifier is deprecated. Add a value parameter to correct this.
    • See this answer for some better ways of triggering the timed updates.

    Here is an updated version of the example. This also illustrates how to use task(id:priority:_:) for the timed animation.

    @State var currentIndex : Int = 0
    //@State var firstString : String = ""
    
    VStack {
        HStack {
            Text("Transform")
    
            Text(array[currentIndex])
                .background(.yellow)
    
            Text("to Quick Note")
        }
        .lineLimit(1)
        .font(.title)
        .minimumScaleFactor(0.5)
        .scaledToFit()
        .padding()
    }
    .animation(.default, value: currentIndex)
    .task(id: currentIndex) {
        try? await Task.sleep(for: .seconds(2))
        if currentIndex == array.count - 1 {
            currentIndex = 0
        } else {
            currentIndex += 1
        }
    }
    

    Animation