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 firstLongPressGesture
hasn't ended yet.The
.second(Bool, Bool?)
case is when the firstLongPressGesture
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'sUpdate 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
Issue with Auto Layout on iOS 8 (Code Works Perfectly on iOS 7)
Why Does Safari Mobile Have Trouble Handling Many Input Fields on iOS 8
Number of Days in the Current Month Using iOS
Xcode 7 How to Refresh Provisioning Profiles
Xcode: Could Not Inspect the Application Package
Use Uibarbuttonitem Icon in Uibutton
Showing a Uipickerview with Uiactionsheet in iOS8 Not Working
What Do JSONserialization Options Do and How Do They Change JSONresult
How to Stop Uitableview from Clipping Uitableviewcell Contents in iOS 7
Complete List of iOS App Permissions
Swift Framework: Umbrella Header '[...].H' Not Found
How to Get Screen Size Using Code on iOS