Search code examples
iosswiftswiftuiswiftui-foreach

How to animate a button getting clicked inside a forEach loop?


I am trying to animate the button when user taps on it. It offsets on the side making it look like it's pressed.

If you look at the images you should see why I want to animate it by offsetting it since I have a background behind the button which is offset and the button should match that frame when clicked.

Currently the button animates as shown in the picture when tapped but all of the button animates getting pressed and they don't return to original position after the click happens.

Button before clicked
Button before clicked

Button after click
Button after click

Below is the buttons array:

@State private var isClicked = false
    
    
    let buttons: [[CalcButton]] = [
        [.clear, .plusMinus, .percent, .divide],
        [.seven, .eight, .nine, .multiply],
        [.four, .five, .six, .minus],
        [.one, .two, .three, .add],
        [.zero, .doubleZero, .decimal, .equals]
    ]
ForEach(buttons, id: \.self) { row in
    HStack(spacing: 20) {
        ForEach(row, id: \.self) { item in
            Button(action: {
                withAnimation {
                    self.animation()
                }
            } , label: {
                ZStack {

                    Rectangle()
                        .frame(width: buttonWidth(button: item), height: buttonHeight())
                        .foregroundColor(.backgroundColor)
                        .offset(x: 7.0, y: 7.0)

                    Rectangle()
                        .frame(width: buttonWidth(button: item), height: buttonHeight())
                        .foregroundColor(.white)


                    Text(item.rawValue)
                        .font(.custom("ChicagoFLF", size: 27))
                        .frame(width: buttonWidth(button: item), height: buttonHeight())
                        .foregroundColor(.backgroundColor)
                        .border(Color.backgroundColor, width: 4)
                        .offset(x: isClicked ? 7 : 0, y: isClicked ? 7 : 0)

                }

            })

        }
    }
    .padding(.bottom, 10)

}

This is the function to toggle the isClicked state variable

func animation() {
    self.isClicked.toggle()
}

Solution

  • You need a selection state for each button. so better to create a custom button.

    Here is the demo version code.

    Custom Button view

    struct CustomButton: View {
        var text: String
        var action: () -> Void
        
        @State private var isPressed = false
        
        var body: some View {
            Button(action: {
                // Do something..
            }, label: {
                ZStack {
                    Rectangle()
                        .foregroundColor(.black)
                        .offset(x: 7.0, y: 7.0)
                    
                    Rectangle()
                        .foregroundColor(.white)
                    
                    Text(text)
                        .frame(width: 50, height: 50)
                        .foregroundColor(.black)
                        .border(Color.black, width: 4)
                        .offset(x: isPressed ? 7 : 0, y: isPressed ? 7 : 0)
                }
                
            })
            .buttonStyle(PlainButtonStyle())
            .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onChanged({ _ in
                        // Comment this line if you want stay effect after clicked
                        isPressed = true
                    })
                    .onEnded({ _ in
                        isPressed = false
                        // // Uncomment below line and comment above line if you want stay effect after clicked
                        //isPressed.toggle()
                        action()
                    })
            )
            .frame(width: 50, height: 50)
        }
    }
    

    Usage:

    struct DemoView: View {
        var body: some View {
            HStack(spacing: 10) {
                ForEach(0..<10) { index in
                    CustomButton(text: index.description) {
                        print("Action")
                    }
                }
            }
        }
    }
    

    enter image description here

    If you want to keep your effect after clicked. Just replace this code part.

    .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onChanged({ _ in
                    })
                    .onEnded({ _ in
                        isPressed.toggle()
                        action()
                    })
            )
    

    enter image description here