I have a button in SwiftUI and I would like to be able to have a different action for \"tap button\" (normal click/tap) and \"long press\".
Is that possible in Swift
Here is my implementation using a modifier:
struct TapAndLongPressModifier: ViewModifier {
@State private var isLongPressing = false
let tapAction: (()->())
let longPressAction: (()->())
func body(content: Content) -> some View {
content
.scaleEffect(isLongPressing ? 0.95 : 1.0)
.onLongPressGesture(minimumDuration: 1.0, pressing: { (isPressing) in
withAnimation {
isLongPressing = isPressing
print(isPressing)
}
}, perform: {
longPressAction()
})
.simultaneousGesture(
TapGesture()
.onEnded { _ in
tapAction()
}
)
}
}
Use it like this on any view:
.modifier(TapAndLongPressModifier(tapAction: { <tap action> },
longPressAction: { <long press action> }))
It just mimics the look a button by scaling the view down a bit. You can put any other effect you want after scaleEffect
to make it look how you want when pressed.
Try this :)
Handles isInactive, isPressing, isLongPress and Tap(Click)
based on this
I tried to make this as a viewmodifier without success. I would like to see an example with @GestureState variable wrapper used in same manner as @State/@Published are bound to @Binding in view components.
Tested: Xcode 12.0 beta, macOS Big Sur 11.0 beta
import SwiftUI
enum PressState {
case inactive
case pressing
case longPress
var isPressing: Bool {
switch self {
case .inactive:
return false
case .pressing, .longPress:
return true
}
}
var isLongPress: Bool {
switch self {
case .inactive, .pressing:
return false
case .longPress:
return true
}
}
var isInactive : Bool {
switch self {
case .inactive:
return true
case .pressing, .longPress:
return false
}
}
}
struct ContentView: View {
@GestureState private var pressState: PressState = PressState.inactive
@State var showClick: Bool = false
var press: some Gesture {
LongPressGesture(minimumDuration: 0.8, maximumDistance: 50.0)
.sequenced(before: LongPressGesture(minimumDuration: .infinity, maximumDistance: 50.0))
.updating($pressState) { value, state, transaction in
switch value {
case .first(true): // first gesture starts
state = PressState.pressing
case .second(true, nil): // first ends, second starts
state = PressState.longPress
default: break
}
}
}
var body: some View {
ZStack{
Group {
Text("Click")
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -120 : -100) : -40)
.animation(Animation.linear(duration: 0.5))
.opacity(showClick ? 1 : 0 )
.animation(Animation.linear(duration: 0.3))
Text("Pressing")
.opacity(pressState.isPressing ? 1 : 0 )
.offset(x: 0, y: pressState.isPressing ? (pressState.isLongPress ? -100 : -80) : -20)
.animation(Animation.linear(duration: 0.5))
Text("Long press")
.opacity(pressState.isLongPress ? 1 : 0 )
.offset(x: 0, y: pressState.isLongPress ? -80 : 0)
.animation(Animation.linear(duration: 0.5))
}
Group{
Image(systemName: pressState.isLongPress ? "face.smiling.fill" : (pressState.isPressing ? "circle.fill" : "circle"))
.offset(x: 0, y: -100)
.font(.system(size: 60))
.opacity(pressState.isLongPress ? 1 : (pressState.isPressing ? 0.6 : 0.2))
.foregroundColor(pressState.isLongPress ? .orange : (pressState.isPressing ? .yellow : .white))
.rotationEffect(.degrees(pressState.isLongPress ? 360 : 0), anchor: .center)
.animation(Animation.linear(duration: 1))
Button(action: {
showClick = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
self.showClick = false
})
}, label: {
ZStack {
Circle()
.fill(self.pressState.isPressing ? Color.blue : Color.orange)
.frame(width: 100, height: 100, alignment: .center)
Text("touch me")
}}).simultaneousGesture(press)
}.offset(x: 0, y: 110)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I just discovered that the effect depends on the order of the implementation. Implementing the detection of gestures in the following order it seems to be possible to detect and identify all three gestures:
Tested on Xcode Version 11.3.1 (11C504)
fileprivate func myView(_ height: CGFloat, _ width: CGFloat) -> some View {
return self.textLabel(height: height, width: width)
.frame(width: width, height: height)
.onTapGesture(count: 2) {
self.action(2)
}
.onLongPressGesture {
self.action(3)
}
.onTapGesture(count: 1) {
self.action(1)
}
}
As a follow up, I had the same issue and I tried all of these answers but didn't like how they all worked. I ended up using a .contextMenu it was way easier and produces pretty much the same effect.
Check link here
and here is an example
This isn't tested, but you can try to add a LongPressGesture
to your button.
It'll presumably look something like this.
struct ContentView: View {
@GestureState var isLongPressed = false
var body: some View {
let longPress = LongPressGesture()
.updating($isLongPressed) { value, state, transaction in
state = value
}
return Button(/*...*/)
.gesture(longPress)
}
}
I tried many things but finally I did something like this:
Button(action: {
}) {
VStack {
Image(self.imageName)
.resizable()
.onTapGesture {
self.action(false)
}
.onLongPressGesture(minimumDuration: 0.1) {
self.action(true)
}
}
}
It is still a button with effects but short and long press are different.