我在SwiftUI中有一个按钮,我希望能够对“点击按钮”(正常点击/点击)和“长按”采取不同的操作。
在SwiftUI中有可能吗?
这是我现在拥有的按钮的简单代码(仅处理“常规”轻触/触摸盒)。
//This looks for columns in first row which has whitespace
//and then goes down the rows to see if it is all whitespace
for (int a=0; a<testCol[0];a++){
for (int b=0;b<testRow[0];b++){
if(a<494){
if (astCheck[b][a] ==' '){
if (astCheck[testRow[0]-1][testCol[0]-1]==' '){
whiteCols[b][a] = true;
}else{
whiteCols[b][a] = false;
continue;
}
}else if (astCheck[b][a]=='*'){
a++;
b=0;
//continue;
}
}
}
}
我已经尝试添加“ longPress手势”,但是它仍然只能“执行”“正常/短”点击。这是我尝试的代码:
Button(action: {self.BLEinfo.startScan() }) {
Text("Scan")
} .disabled(self.BLEinfo.isScanning)
谢谢!
杰拉德
答案 0 :(得分:6)
我刚刚发现效果取决于实现的顺序。按照以下顺序实现手势检测,似乎可以检测和识别所有三个手势:
在Xcode版本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)
}
}
答案 1 :(得分:3)
我尝试了很多事情,但最终我做了这样的事情:
Button(action: {
}) {
VStack {
Image(self.imageName)
.resizable()
.onTapGesture {
self.action(false)
}
.onLongPressGesture(minimumDuration: 0.1) {
self.action(true)
}
}
}
它仍然是带有效果的按钮,但是长按和短按是不同的。
答案 2 :(得分:3)
我必须为正在构建的应用执行此操作,因此只想共享。请参阅底部的代码,它相对来说是自解释的,并且坚持SwiftUI的主要元素。
此答案与上述答案的主要区别在于,它允许根据状态更新按钮的背景色,并且还涵盖了一种使用情况,即希望长按的动作在手指被抬起而不是在手指抬起时发生时间阈值已通过。
正如其他人所指出的那样,我无法直接将手势应用于按钮,而不得不将其应用于其内部的文本视图。不幸的是,它具有减少按钮“点击框”的副作用,如果我在按钮边缘附近按下,手势将不会触发。因此,我删除了Button,专注于直接操作我的Text View对象(可以用Image View或其他视图替换(但不能替换Button!))。
下面的代码设置了三个手势:
可立即触发并反映您问题中“轻击”手势的LongPressGesture(我尚未测试过,但可以用TapGesture代替)
另一种LongPressGesture,其最小持续时间为0.25,可反映您问题中的“长按”手势
一个最小距离为0的拖动手势,允许我们在手指的末端从按钮上抬起而不是在0.25秒时自动执行事件(如果不是您的用例,可以将其删除)。您可以在此处了解更多信息:How do you detect a SwiftUI touchDown event with no movement or duration?
我们按以下顺序对手势进行排序:使用“排他性”组合“长按”(即上面的2和3)和Tap(上面的第一个手势),如果“长按”的0.25秒阈值不是到达后,将执行轻击手势。 “长按”本身是我们的长按手势和拖动手势的序列,因此只有在手指抬起后才能执行该动作。
我还在下面添加了代码,用于根据状态更新按钮的颜色。需要注意的一件事是,我不得不将按钮颜色的代码添加到长按和拖动手势的onEnded部分中,因为不幸的是,处理时间的缩短会导致按钮在longPressGesture和DragGesture(从理论上讲这是不会发生的,除非我的某个地方有错误!)。
您可以在此处了解有关手势的更多信息:https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures
如果您修改以下内容并注意Apple的手势说明(此答案也很有用,请阅读:How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?),则应该能够设置复杂的自定义按钮交互。将手势用作构建基块,并将它们组合起来以消除单个手势中的任何不足(例如,longPressGesture不能选择在事件结束时(而不是在达到条件时)进行事件)。
P.S。我有一个全局环境对象“ dataRouter”(与问题无关,以及与我如何选择在多个视图之间共享参数),可以安全地将其删除。
struct AdvanceButton: View {
@EnvironmentObject var dataRouter: DataRouter
@State var width: CGFloat
@State var height: CGFloat
@State var bgColor: Color
@GestureState var longPress = false
@GestureState var longDrag = false
var body: some View {
let longPressGestureDelay = DragGesture(minimumDistance: 0)
.updating($longDrag) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { value in
print(value.translation) // We can use value.translation to see how far away our finger moved and accordingly cancel the action (code not shown here)
print("long press action goes here")
self.bgColor = self.dataRouter.darkButton
}
let shortPressGesture = LongPressGesture(minimumDuration: 0)
.onEnded { _ in
print("short press goes here")
}
let longTapGesture = LongPressGesture(minimumDuration: 0.25)
.updating($longPress) { currentstate, gestureState, transaction in
gestureState = true
}
.onEnded { _ in
self.bgColor = self.dataRouter.lightButton
}
let tapBeforeLongGestures = longTapGesture.sequenced(before:longPressGestureDelay).exclusively(before: shortPressGesture)
return
Text("9")
.font(self.dataRouter.fontStyle)
.foregroundColor(self.dataRouter.darkButtonText)
.frame(width: width, height: height)
.background(self.longPress ? self.dataRouter.lightButton : (self.longDrag ? self.dataRouter.brightButton : self.bgColor))
.cornerRadius(15)
.gesture(tapBeforeLongGestures)
}
}
答案 3 :(得分:2)
结合高优先级手势和同步手势应该可以解决问题。
Button(action: {})
{
Text("A Button")
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
print("Loooong")
}
)
.highPriorityGesture(TapGesture()
.onEnded { _ in
print("Tap")
})
在与其他视图交互时也发现这是一个方便的模式。
答案 4 :(得分:1)
未经测试,但是您可以尝试向按钮中添加LongPressGesture
。
大概看起来像这样。
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)
}
}
答案 5 :(得分:0)
如果其他任何人都在挣扎,我想将其发布回去。奇怪的是,Apple的默认行为适用于大多数控件,但不适用于按钮。就我而言,我想在支持长按的同时保持按钮效果。
一种无需太多复杂性就能起作用的方法是忽略默认按钮动作,并创建一个同时处理正常和长按的手势。
在您看来,您可以应用自定义长按修饰符,如下所示:
var body: some View {
// Apply the modifier
Button(action: self.onReloadDefaultAction) {
Text("Reload")
}
.modifier(LongPressModifier(
isDisabled: self.sessionButtonsDisabled,
completionHandler: self.onReloadPressed))
}
// Ignore the default click
private func onReloadDefaultAction() {
}
// Handle the simultaneous gesture
private func onReloadPressed(isLongPress: Bool) {
// Do the work here
}
我的长按修饰符实现看起来像这样,并使用了我从另一篇文章中找到的拖动手势。虽然不是很直观,但是可以可靠地运行,尽管我当然不希望自己编写这个管道。
struct LongPressModifier: ViewModifier {
// Mutable state
@State private var startTime: Date?
// Properties
private let isDisabled: Bool
private let longPressSeconds: Double
private let completionHandler: (Bool) -> Void
// Initialise long press behaviour to 2 seconds
init(isDisabled: Bool, completionHandler: @escaping (Bool) -> Void) {
self.isDisabled = isDisabled
self.longPressSeconds = 2.0
self.completionHandler = completionHandler
}
// Capture the start and end times
func body(content: Content) -> some View {
content.simultaneousGesture(DragGesture(minimumDistance: 0)
.onChanged { _ in
if self.isDisabled {
return
}
// Record the start time at the time we are clicked
if self.startTime == nil {
self.startTime = Date()
}
}
.onEnded { _ in
if self.isDisabled {
return
}
// Measure the time elapsed and reset
let endTime = Date()
let interval = self.startTime!.distance(to: endTime)
self.startTime = nil
// Return a boolean indicating whether a normal or long press
let isLongPress = !interval.isLess(than: self.longPressSeconds)
self.completionHandler(isLongPress)
})
}
}
答案 6 :(得分:0)
尝试一下:)
句柄isInactive,isPressing,isLongPress和Tap(单击)
基于this
我试图使它作为viewmodifier失败。我想看到一个示例,其中@GestureState变量包装器的使用方式与@ State / @ Published在视图组件中绑定到@Binding相同。
经过测试: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()
}
}
答案 7 :(得分:0)
答案 8 :(得分:0)
这是我使用修饰符的实现:
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()
}
)
}
}
在任何视图上都使用它:
.modifier(TapAndLongPressModifier(tapAction: { <tap action> },
longPressAction: { <long press action> }))
它只是通过稍微缩小视图来模仿按钮的外观。您可以在scaleEffect
之后加上其他想要的效果,使其在按下时看起来像您想要的。
答案 9 :(得分:0)
只需这样做: 第一个修饰符应该是 onLongPressGesture(minumumDuration: (你想要的持续时间)) 并且以下修饰符应该是 onLongPressGesture(minimumDuration: 0.01) <- 或其他一些超小数字
这对我很有效