buttonswiftuigesturetapgamepad

SwiftUI - How to perform action only while the button is tapped, and end it when the tap is released?


I'm trying to recreate a Game Boy like game pad in SwiftUI. Graphically it looks good, but I can't make the actions work. I would like it to perform the action (move in the selected direction) while the arrow is tapped, and to stop moving once the arrow isn't tapped anymore (just like a real game pad would). The code I tried so far is this one:

import SwiftUI

struct GamePad: View {
    
    @State var direction = "Empty"
    @State var animate = false
    
    var body: some View {
        ZStack {
            VStack {
                Text("\(direction) + \(String(describing: animate))")
                    .padding()
                Spacer()
            }
            VStack(spacing: 0) {
                Rectangle()
                    .frame(width: 35, height: 60)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay(
                        Button {
                            direction = "Up"
                            animate = true
                        } label: {
                            VStack {
                                Image(systemName: "arrowtriangle.up.fill")
                                    .foregroundColor(.black.opacity(0.4))
                                Spacer()
                            }
                            .padding(.top, 10)
                            .gesture(
                                TapGesture()
                                    .onEnded({ () in
                                        direction = "Ended"
                                        animate = false
                                    })
                            )
                        }
                    )
                
                Rectangle()
                    .frame(width: 35, height: 60)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay(
                        Button {
                            direction = "Down"
                            animate = true

                        } label: {
                            VStack {
                                Spacer()
                                Image(systemName: "arrowtriangle.down.fill")
                                    .foregroundColor(.black.opacity(0.4))
                            }
                                .padding(.bottom, 10)
                                .gesture(
                                    TapGesture()
                                        .onEnded({ () in
                                            direction = "Ended"
                                            animate = false
                                        })
                                )
                        }
                    )
            }
            HStack(spacing: 35) {
                Rectangle()
                    .frame(width: 43, height: 35)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay(
                        Button {
                            direction = "Left"
                            animate = true

                        } label: {
                            VStack {
                                Image(systemName: "arrowtriangle.left.fill")
                                    .foregroundColor(.black.opacity(0.4))
                                Spacer()
                            }
                                .padding(.top, 10)
                                .gesture(
                                    TapGesture()
                                        .onEnded({ () in
                                            direction = "Ended"
                                            animate = false
                                        })
                                )
                        }
                    )
                Rectangle()
                    .frame(width: 43, height: 35)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay(
                        Button {
                            direction = "Right"
                            animate = true

                        } label: {
                            VStack {
                                Spacer()
                                Image(systemName: "arrowtriangle.right.fill")
                                    .foregroundColor(.black.opacity(0.4))
                            }
                                .padding(.bottom, 10)
                                .gesture(
                                    TapGesture()
                                        .onEnded({ () in
                                            direction = "Ended"
                                            animate = false
                                        })
                                )
                        }
                    )
            }
        }
    }
}

What am I doing wrong? Thanks


Solution

  • It can be achieved with custom button style, because it has isPressed state in configuration.

    Here is a demo of possible solution. Tested with Xcode 13.4 / iOS 15.5

    demo

    struct StateButtonStyle: ButtonStyle {
        var onStateChanged: (Bool) -> Void
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .opacity(configuration.isPressed ? 0.5 : 1)  // << press effect
                .onChange(of: configuration.isPressed) {
                    onStateChanged($0)  // << report if pressed externally
                }
        }
    }
    

    and updated button with it

        Button {
            direction = "Ended" // action on touchUP
        } label: {
            VStack {
                Image(systemName: "arrowtriangle.up.fill")
                    .foregroundColor(.black.opacity(0.4))
                Spacer()
            }
            .padding(.top, 10)
        }
        .buttonStyle(StateButtonStyle { // << press state is here !!
            animate = $0
            if $0 {
                direction = "Up"
            }
        })
    

    Test module on GitHub