xcodeanimationswiftui

Delay between buttons appearing in HStack


I'm trying to get the labels on 5 buttons in an HStack to appear one at a time with a 1 second delay between them appearing. In my code they all appear at once. Obviously I am not using sleep correctly. Not sure I should be using it at all. In other words I would like the "A" to appear, then one second later the "B" etc.

    HStack (spacing: 25) {
        ForEach(letters, id: \.self) { letter in
            Button {

            }
            label : {
                ZStack {
                    RoundedRectangle(cornerRadius: 15)
                        .fill(Color.blue.opacity(0.3))
                        .frame(width: 50, height: 50)
                    Text(letter)
                        .font(.largeTitle)
                        .foregroundColor(.black)
                }
            }
            .task {
                /// Delay of 1 second
                try? await Task.sleep(nanoseconds: 1_000_000_000)
            }
            
        }
    }

enter image description here


Solution

  • Currently, the task in your example is sleeping for 1 second, but then nothing happens afterwards. So at the moment, the task is serving no purpose.

    To see an animation happen, there needs to be a change of state. A typical technique is to update a state variable that changes the view. This change can then be animated.

    Here are a couple of ways of implementing a staggered animation. In both cases, the visibility of the letters is controlled by changing the opacity.

    1. Trigger all animations together, then use .delay to stagger the individual animations

    @State private var isShowing = false
    
    HStack (spacing: 25) {
        ForEach(Array(letters.enumerated()), id: \.offset) { index, letter in
            Button {} label: {
                ZStack {
                    // ...
                }
            }
            .opacity(isShowing ? 1 : 0)
            .animation(.easeInOut.delay((Double(index + 1) * 1.0)), value: isShowing)
        }
    }
    .onAppear {
        isShowing = true
    }
    

    2. Use .task(id:) to increment a counter

    This approach is closer to what you had in your example:

    @State private var nVisibleLetters = 0
    
    HStack (spacing: 25) {
        ForEach(Array(letters.enumerated()), id: \.offset) { index, letter in
            Button {} label: {
                ZStack {
                    // ...
                }
            }
            .opacity(nVisibleLetters > index ? 1 : 0)
            .animation(.easeInOut, value: nVisibleLetters)
        }
    }
    .task(id: nVisibleLetters) {
        try? await Task.sleep(for: .seconds(1))
        if nVisibleLetters < letters.count {
            nVisibleLetters += 1
        }
    }
    

    The animation looks the same in both cases:

    Animation