Home > Back-end >  SwiftUI animate text component when value changes
SwiftUI animate text component when value changes

Time:10-28

Assuming I have a SwiftUI component that display two numbers. The following sample code simulate a simple situation where the two numbers is updated from time to time randomly.

struct ContentView: View {
    @State var number1: Int = 0
    @State var number2: Int = 0
    var timer:Timer {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if (Int.random(in: 0...100)<10) {
                print("random event happened, increase numbers!")
                self.number1 = self.number1   1
                self.number2 = self.number2   1
            }
        }
    }
    var body: some View {
        ZStack {
            SampleNumbersView(numbers1: number1, numbers2: number2)
        }.onAppear {
            let _ = self.timer
        }
    }
}

struct SampleNumbersView: View {
    var numbers1:Int = 0
    var numbers2:Int = 0
    var body: some View {
        VStack {
            Text("Number 1: \(numbers1)")
            Text("Number 2: \(numbers2)")
        }
    }
}

The above code works. However, what should I do if I want the following animation sequence to happens whenever the two numbers are updated?

  1. First display the updated number 1
  2. 1 second later display the updated number 2
  3. 1 second later proceed to blink the two numbers a few times.

In theory the function to animate is roughly like this

    func updateNumber1and2(number1: Int, number2:Int) {
    //first show number1
    self.number1 = number1
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
        //next show number 2
        self.number2 = number2

        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
            //next blink text twice
            print("start blinking")
            self.numbersBlinking = true
            Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
                print("stop blinking")
                self.numbersBlinking = false
            }
        }
    }
}

But I couldnt figure out when or how to execute a function like this in the SwiftUI component?

CodePudding user response:

SwiftUI has a built animation functions, and they are triggered by a change in a variable. You wouldn't have to roll your own. And certainly not with multiple timers. If you were to use timers, you could use one and just keep track of when things changed based on the one timer. Multiple timers are not recommended.

Because of your listed requirements, this code is a bit more complicated than a normal animation, but you wanted the delays. If you just wanted the view to blink in the change, I would have simply wrapped animator in the withAnimation() block and it would have blinked immediately upon the change.

struct AnimatedTextView: View {
    @State var number1: Int = 0
    @State var number2: Int = 0
    @State var animator = false
    
    var body: some View {
        VStack {
            Button { // Changed your timer to a button so that you don't have to wait for some random event
                self.number1 = self.number1   1
                // This delays the execution by 1 second.
                DispatchQueue.main.asyncAfter(deadline: .now()   1) {
                    self.number2 = self.number2   1
                }
                // This delays the execution by 2 seconds.
                DispatchQueue.main.asyncAfter(deadline: .now()   2) {
                    withAnimation(.easeInOut(duration: 0.2).repeatCount(10)) {
                        // Changing this to true causes the opacity of the view to go to 0.
                        // By putting it in an animated block, the change will be slowed and repeated 5 times
                        // On and off each separately count as a time
                        animator = true
                    }
                    // This delays the execution by 1 second after the animator variable is changed.
                    DispatchQueue.main.asyncAfter(deadline: .now()   1) {
                        // This gives time for the animation to complete and then sets the opacity back to 1
                        // Otherwise the view will be hidden. It is essentially a reset.
                        animator = false
                    }
                }
                
            } label: {
                Text("Change Numbers")
            }
            
            SampleNumbersView(numbers1: number1, numbers2: number2)
                // animator is used here to change the opacity, and it is animated repeatedly
                // causing the view to blink.
                .opacity(animator ? 0 : 1)
        }
    }
}

struct SampleNumbersView: View {
    // Since you are not changing the numbers in this view, declare them as let
    // and do not give them an initial value. There is less system overhead.
    let numbers1: Int
    let numbers2: Int
    
    var body: some View {
        VStack {
            Text("Number 1: \(numbers1)")
            Text("Number 2: \(numbers2)")
        }
    }
}

And, to answer your original question, if you were on iOS 14 or later, you would use .onChange(of:perform:) on one of the views to catch the update and run your function like this:

        .onChange(of: number1) { _ in
            updateNumber1and2(number1: number1, number2:number2)
        }
  • Related