11

For SwiftUI the default button behavior is equivalent to UIKit's "touch up inside", which activates when your finger touches the button then raises while within the bounds of the button.

Is there any way to change this to "touch down" so the action closure is run immediately when your finger touches the button?

RogerTheShrubber
  • 684
  • 6
  • 18

5 Answers5

12

You can use a DragGesture with a minimumDistance of zero and define a closure for DOWN (onChanged()) or UP (onEnded()):

struct ContentView: View {
    @State private var idx = 0

    var body: some View {
        let g = DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({
            print("DOWN: \($0)")
        }).onEnded({
            print("UP: \($0)")
        })

        return Rectangle().frame(width: 100, height: 50).gesture(g)
    }
}
kontiki
  • 32,877
  • 11
  • 99
  • 118
7

You can create a custom view modifier:

extension View {
    func onTouchDownGesture(callback: @escaping () -> Void) -> some View {
        modifier(OnTouchDownGestureModifier(callback: callback))
    }
}

private struct OnTouchDownGestureModifier: ViewModifier {
    @State private var tapped = false
    let callback: () -> Void

    func body(content: Content) -> some View {
        content
            .simultaneousGesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
                    if !self.tapped {
                        self.tapped = true
                        self.callback()
                    }
                }
                .onEnded { _ in
                    self.tapped = false
                })
    }
}

struct MyView: View {
    var body: some View {
        Text("Hello World")
            .onTouchDownGesture {
                print("View did tap!")
            }
    }
}
matteopuc
  • 9,667
  • 6
  • 56
  • 84
2

You can use a hidden _onButtonGesture method on View, which is public. It doesn't even need to be attached to the Button, but it looks better since you see that pressing down effect.

Code:

struct ContentView: View {
    @State private var counter = 0
    @State private var pressing = false

    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(counter)")

            Button("Increment") {
                counter += 1
            }
            ._onButtonGesture { pressing in
                self.pressing = pressing
            } perform: {}

            Text("Pressing button: \(pressing ? "yes" : "no")")
        }
    }
}

Result:

Result

Doesn't look great as a GIF due to the frame rate, but whenever you press down pressing goes to true, then false on release.

George
  • 19,234
  • 7
  • 57
  • 99
  • FYI, a property prefixed with an underscore means Apple doesn't intend for it to be used in production. – Patrick Dec 28 '21 at 18:46
0

While DragGesture works well in many scenarios, it can have some unwanted side effects such as when used within a scroll view, it will take precedence over the scroll view's built-in gesture handing.

In SwiftUI, Buttons already keep track of the pressed state, so a solution to this problem is to use a custom ButtonStyle to allow for hooking into changes in the isPressed state.

Here's a working solution:

Define a custom ButtonStyle:

struct CustomButtonStyle: ButtonStyle {
    
    var onPressed: () -> Void
    
    var onReleased: () -> Void
    
    // Wrapper for isPressed where we can run custom logic via didSet (or willSet)
    @State private var isPressedWrapper: Bool = false {
        didSet {
            // new value is pressed, old value is not pressed -> switching to pressed state
            if (isPressedWrapper && !oldValue) {
                onPressed()
            } 
            // new value is not pressed, old value is not pressed -> switching to unpressed state
            else if (oldValue && !isPressedWrapper) {
                onReleased()
            }
        }
    }
    
    // return the label unaltered, but add a hook to watch changes in configuration.isPressed
    func makeBody(configuration: Self.Configuration) -> some View {
        return configuration.label
            .onChange(of: configuration.isPressed, perform: { newValue in isPressedWrapper = newValue })
    }
}

You could also write the didSet logic directly in the perform block of the onChange modifier, but I think this keeps it looking clean.

Wrap your clickable view with Button


struct ExampleView: View {

    @State private var text: String = "Unpressed"

    var body: some View {
        Text(self.text)
        Button(action: { ... }, label: {
            // your content here
        }).buttonStyle(CustomButtonStyle(onPressed: { 
            self.text = "Pressed!"
        }, onReleased: { 
            self.text = "Unpressed" 
        }))
    }
}

Then you can pass whatever logic you want to the onPressed and onReleased parameters to CustomButtonStyle.

I've used this to allow for custom onPressed handling of clickable rows in a ScrollView.

akerra
  • 652
  • 4
  • 17
-1

I managed to achieve that with a simple button modifier:

struct TouchedButtonStyle: PrimitiveButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration
            .label
            .onTapGesture(perform: configuration.trigger)
    }
}

Now you have to just assign the modifier to your button:

YourButton()
    .buttonStyle(TouchedButtonStyle())
Vitalii Vashchenko
  • 1,693
  • 1
  • 13
  • 22