SwiftUI Rolling Text Animation Freezes with Rapid User Input

60 views Asked by At

I'm working on a counter app in SwiftUI where I have implemented a rolling text animation for the numbers. The animation works fine when the number is incremented one at a time. However, when the user rapidly increases the number by multiple inputs in quick succession, the animation freezes momentarily before resuming.

Here's the code for my RollingText view:

//
//  RollingText.swift
//

import SwiftUI

struct RollingText: View {
    var font: Font = .largeTitle
    var weight: Font.Weight = .regular

    @Binding var value: Int
    @State var animationRange: [Int] = []
    @State var isAnimating = false

    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<animationRange.count, id: \.self) { index in
                Text("8")
                    .font(font)
                    .fontWeight(weight)
                    .opacity(0)
                    .overlay {
                        GeometryReader { proxy in
                            let size = proxy.size

                            VStack(spacing: 0) {
                                ForEach(0...9, id: \.self) { number in
                                    Text("\(number)")
                                        .font(font)
                                        .fontWeight(weight)
                                        .frame(width: size.width, height: size.height, alignment: .center)
                                }
                            }
                            .offset(y: -CGFloat(animationRange[index]) * size.height)
                        }
                        .clipped()
                    }
            }
        }
        .onAppear {
            animationRange = Array(repeating: 0, count: "\(value)".count)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
                updateText()
            }
        }
        .onChange(of: value) { newValue in
            // 새 값에 따라 animationRange의 길이를 조정합니다.
            adjustAnimationRange(for: newValue)

            // 진행 중인 애니메이션에 대한 목표 값을 업데이트합니다.
            if isAnimating {
                for (index, value) in zip(0..<"\(newValue)".count, "\(newValue)") {
                    animationRange[index] = (String(value) as NSString).integerValue
                }
            } else {
                updateText()
            }
        }
    }
    
    func adjustAnimationRange(for newValue: Int) {
        let newCount = "\(newValue)".count
        let extra = newCount - animationRange.count
        if extra > 0 {
            animationRange += Array(repeating: 0, count: extra)
        } else if extra < 0 {
            animationRange.removeLast(-extra)
        }
    }

    func updateText() {
        isAnimating = true
        let stringValue = "\(value)"
        var longestAnimationDuration = 0.0

        for (index, value) in zip(0..<stringValue.count, stringValue) {
            let fraction = min(Double(index) * 0.15, 0.5)
            let animationDuration = 0.8 + fraction  // 이 값은 애니메이션의 실제 시간에 따라 조정해야 합니다.

            withAnimation(.interactiveSpring(response: 0.8, dampingFraction: 1 + fraction, blendDuration: 1 + fraction)) {
                animationRange[index] = (String(value) as NSString).integerValue
            }

            if animationDuration > longestAnimationDuration {
                longestAnimationDuration = animationDuration
            }
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + longestAnimationDuration) {
            isAnimating = false
        }
    }

}

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

The issue seems to arise when there's fast, repeated user interaction. The animation gets stuck for a short period and then continues. I'm looking for a way to make the animation smooth and continuous, even with rapid user inputs.

How can I modify my SwiftUI code to handle rapid changes in the counter value without interrupting the rolling animation effect?

1

There are 1 answers

0
Harrison Cho On

I understand what you're aiming for, and it seems like the issue might be arising due to the branching logic in that particular section. To reflect changing numbers smoothly, instead of abruptly stopping the current animation, it would be visually more natural to let the existing animation complete fully before starting a new one. Try removing the if statement in that section and give it a go.

 // And since the original method has been deprecated, I recommend using this approach.
        .onChange(of: value) { oldValue, newValue in
            adjustAnimationRange(for: newValue)
            
            // remove 'if' statement and just updateText
            updateText()
        }

If the problem you're describing isn't this, then it's likely a throttling issue due to continuous function calls. In that case, you might need to increase the delay further or explore more robust ways of managing animation states. I hope my suggestion proves helpful.