Button Tap and Long Press Gesture

Button with double action (tap & long press) in SwiftUI

Please check if this works for you:

Button("Button") {
print("tapped")
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("long pressed")
})

Please note that tap action is executed after every long press in the above code. You can handle this with a simple Bool.

Button tap and long press gesture

Hard to say what´s not working with your code, with the only two rows that you have provided, but I would recommend you to do it in this way instead:

Create an outlet to your button instead

@IBOutlet weak var myBtn: UIButton!

And in your viewDidLoad() add the gestures to the buttons

let tapGesture = UITapGestureRecognizer(target: self, action: "normalTap")  
let longGesture = UILongPressGestureRecognizer(target: self, action: "longTap:")
tapGesture.numberOfTapsRequired = 1
myBtn.addGestureRecognizer(tapGesture)
myBtn.addGestureRecognizer(longGesture)

And then create the actions to handle the taps

func normalTap(){

print("Normal tap")
}

func longTap(sender : UIGestureRecognizer){
print("Long tap")
if sender.state == .Ended {
print("UIGestureRecognizerStateEnded")
//Do Whatever You want on End of Gesture
}
else if sender.state == .Began {
print("UIGestureRecognizerStateBegan.")
//Do Whatever You want on Began of Gesture
}
}

Swift 3.0 version:

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.normalTap))
let longGesture = UILongPressGestureRecognizer(target: self, action: Selector(("longTap:")))
tapGesture.numberOfTapsRequired = 1
myBtn.addGestureRecognizer(tapGesture)
myBtn.addGestureRecognizer(longGesture)

func normalTap(){

print("Normal tap")
}

func longTap(sender : UIGestureRecognizer){
print("Long tap")
if sender.state == .ended {
print("UIGestureRecognizerStateEnded")
//Do Whatever You want on End of Gesture
}
else if sender.state == .began {
print("UIGestureRecognizerStateBegan.")
//Do Whatever You want on Began of Gesture
}
}

Updated syntax for Swift 5.x:

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(normalTap))
button.addGestureRecognizer(tapGesture)

let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(longTap))
button.addGestureRecognizer(longGesture)

@objc func normalTap(_ sender: UIGestureRecognizer){
print("Normal tap")
}

@objc func longTap(_ sender: UIGestureRecognizer){
print("Long tap")
if sender.state == .ended {
print("UIGestureRecognizerStateEnded")
//Do Whatever You want on End of Gesture
}
else if sender.state == .began {
print("UIGestureRecognizerStateBegan.")
//Do Whatever You want on Began of Gesture
}
}

How to run function when long press gesture detected and stopped?

I could solve my problem, hope it's useful for some of you:

@State private var isPressingDown: Bool = false

Image(systemName: "mic.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70)
.foregroundColor(Color.blue)
.onLongPressGesture(minimumDuration: 0.3){
self.isPressingDown = true
print("started")
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onEnded{ _ in
if self.isPressingDown{
self.isPressingDown = false
print("ended")
}
}
)

figure out which button was pressed while differ between long-press and tap. swift

The main issue, in this case, is both gestures will be added ONLY for the largeOption button! To clarify, the gesture is added only for one component, which in your case, it should be added only for the latest one (which is largeOption):

smallOption.addGestureRecognizer(tapGesture) <-- skipped
mediumOption.addGestureRecognizer(tapGesture) <-- skipped
largeOption.addGestureRecognizer(tapGesture) <-- added
smallOption.addGestureRecognizer(longGesture) <-- skipped
mediumOption.addGestureRecognizer(longGesture) <-- skipped
largeOption.addGestureRecognizer(longGesture) <-- added

Logically speaking, this might be the answer to your question:

Is it possible to find out what button was pressed in the tap and long function, or will I need to do two functions for each button?

you need to add two gestures for each button because a particular gesture can only be added to one view.

However, you don't have to declare new action methods in addition to @objc func tap(_ sender: UIGestureRecognizer) and @objc func long(_ sender: UIGestureRecognizer) existing ones. What you could do instead is to check the sender's view. Example:

Let's assume that we manually added tow gestures for each button:

// gestures:
let smallOptionTapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
let smallOptionLongGesture = UILongPressGestureRecognizer(target: self, action: #selector(long))
smallOptionLongGesture.minimumPressDuration = 0.5

let mediumOptionTapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
let mediumOptionLongGesture = UILongPressGestureRecognizer(target: self, action: #selector(long))
mediumOptionLongGesture.minimumPressDuration = 0.5

let largeOptionTapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
let largeOptionLongGesture = UILongPressGestureRecognizer(target: self, action: #selector(long))
largeOptionLongGesture.minimumPressDuration = 0.5

// adding them:
smallOption.addGestureRecognizer(smallOptionTapGesture)
mediumOption.addGestureRecognizer(mediumOptionTapGesture)
largeOption.addGestureRecognizer(largeOptionTapGesture)

smallOption.addGestureRecognizer(smallOptionLongGesture)
mediumOption.addGestureRecognizer(mediumOptionLongGesture)
largeOption.addGestureRecognizer(largeOptionLongGesture)

Therefore, what you could do is:

@objc func tap(_ sender: UIGestureRecognizer) {
// an example of how you could check the button
if sender.view == smallOption {
print("small short-press")
} else if sender.view == mediumOption {
print("medium short-press")
} else if sender.view == largeOption {
print("large short-press")
}
}
@objc func long(_ sender: UIGestureRecognizer) {
// you could apply the same above approach here
}

The other option is to create action methods for each button separately.

On Tap and On Long Press not working properly

You can just put your gestures actions on Image and there is s no need to be on Button in this purpose.

How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?

I managed to solve it, although if anyone has an easier solution I would gladly accept.

Basically I need to chain 2 LongPressGesture-s together.

The first one will take effect after a 2 second long press - this is when the something should appear.

The second one will take effect after Double.infinity time, meaning that it will never complete, so the user can press as long as they want. For this effect, we only care about the event when it is cancelled - meaning that the user stopped pressing.

@GestureState private var isPressingDown: Bool = false

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
.sequenced(before: LongPressGesture(minimumDuration: .infinity))
.updating($isPressingDown) { value, state, transaction in
switch value {
case .second(true, nil): //This means the first Gesture completed
state = true //Update the GestureState
default: break
}
})
.
[...]

something.opacity(isPressingDown ? 1 : 0)

When sequencing two LongPressGesture-s by calling the .sequenced(before:) method, you get a

SequenceGesture<LongPressGesture, LongPressGesture> as return value

which has a .first(Bool) and a .second(Bool, Bool?) case in its Value enum.

The .first(Bool) case is when the first LongPressGesture hasn't ended yet.

The .second(Bool, Bool?) case is when the first LongPressGesture has ended.

So when the SequenceGesture's value is .second(true, nil), that means the first Gesture has completed and the second is yet undefined - this is when that something should be shown - this is why we set the state variable to true inside that case (The state variable encapsulates the isPressingDown variable because it was given as first parameter to the .updating(_:body:) method).

And we don't have to do anything about setting the state back to false because when using the .updating(_:body:) method the state returns to its initial value - which was false - if the user cancels the Gesture. Which will result in the disappearance of "something". (Here cancelling means we lift our finger before the minimum required seconds for the Gesture to end - which is infinity seconds for the second gesture.)

So it is important to note that the .updating(_:body:) method's callback is not called when the Gesture is cancelled, as per this documentation's Update Transient UI State section.


EDIT 03/24/2021:

I ran into the problem of updating an @Published property of an @ObservedObject in my view. Since the .updating() method closure is not called when resetting the GestureState you need another way to reset the @Published property. The way to solve that issue is adding another View Modifier called .onChange(of:perform:):

Model.swift:

class Model: ObservableObject {
@Published isPressedDown: Bool = false

private var cancellableSet = Set<AnyCancellable>()

init() {
//Do Something with isPressedDown
$isPressedDown
.sink { ... }
.store(in: &cancellableSet)
}
}

View.swift:

@GestureState private var _isPressingDown: Bool = false
@ObservedObject var model: Model

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
.sequenced(before: LongPressGesture(minimumDuration: .infinity))
.updating($_isPressingDown) { value, state, transaction in
switch value {
case .second(true, nil): //This means the first Gesture completed
state = true //Update the GestureState
model.isPressedDown = true //Update the @ObservedObject property
default: break
}
})
.onChange(of: _isPressingDown) { value in
if !value {
model.isPressedDown = false //Reset the @ObservedObject property
}
})

SwiftUI: Longpress Gesture Hold for only 1 Second

here is a very basic approach that you can build on, based on the code in:

https://adampaxton.com/make-a-press-and-hold-fast-forward-button-in-swiftui/

struct IgnitionDriveView: View {

@State private var timer: Timer?
@State var isLongPressD = false
@State var isLongPressR = false

@State private var showDriveAlert = true
@State private var showOutOfGasAlert = false
@State var distanceCovered: Float = 0.0

private func circleShape(isPressed: Binding<Bool>) -> some View {
Button(action: {
if isPressed.wrappedValue {
isPressed.wrappedValue.toggle()
timer?.invalidate()
}
}) {
ZStack {
Circle().strokeBorder(style: StrokeStyle(lineWidth: 2))
Circle().fill(isPressed.wrappedValue ? .white : .red)
}.frame(width: 100, height: 100, alignment: .center)
}
}

var body: some View {

VStack(alignment: .leading) {
Text("Distance Covered in Km: \(distanceCovered)").font(.headline)
ProgressView(value: distanceCovered > 0 ? distanceCovered : 0, total: 1000).frame(height: 40)

HStack {
ZStack {
circleShape(isPressed: $isLongPressD)
.simultaneousGesture(LongPressGesture(minimumDuration: 0.2).onEnded { _ in
isLongPressD = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
if distanceCovered < 1000 {
distanceCovered += 10
} else {
showOutOfGasAlert = true
}
})
})

Text("D").bold().padding().foregroundColor(.green).font(.title)
}.foregroundColor(.green)

Spacer()

ZStack {
circleShape(isPressed: $isLongPressR)
.simultaneousGesture(LongPressGesture(minimumDuration: 0.2).onEnded { _ in
isLongPressR = true
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
if distanceCovered > 0 {
distanceCovered -= 10
}
})
})

Text("R").bold().padding().foregroundColor(.blue).font(.title)
}.foregroundColor(.green)

}.padding()
}.alert("Press D to Drive and R to Reverse", isPresented: $showDriveAlert) {
Button("Okay") { showDriveAlert = false }
}.alert("You ran out of Gas, Reverse to Gas Station", isPresented: $showOutOfGasAlert) {
Button("Sucks, but fine!") { showOutOfGasAlert = false }
}
.padding()
}
}


Related Topics



Leave a reply



Submit