Pandruz
Pandruz

Reputation: 439

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

Upvotes: 3

Views: 1142

Answers (2)

eastriver lee
eastriver lee

Reputation: 498

by setting minimumDuration of onLongPressGesture as .infinity, you can achieve the effect that you desire:

extension View {
    func onHold(perform action: @escaping (Bool) -> Void) -> some View {
        onLongPressGesture(minimumDuration: .infinity, perform: {}) { isPressing in
            action(isPressing)
        }
    }
}

usage

struct TestView: View {
    @State var isHolding = false
    var body: some View {
        Text("😉")
            .blur(radius: isHolding ? 0 : 10)
            .animation(.easeIn, value: isHolding)
            .onHold(perform: { isHolding = $0 })
    }
}

Upvotes: 1

Asperi
Asperi

Reputation: 257493

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

Upvotes: 3

Related Questions