SwiftUI:如何处理按钮的短按和长按?

67

我在SwiftUI中有一个按钮,希望在“点击按钮”(正常点击/触摸)和“长按”时具有不同的操作。

在SwiftUI中是否可能实现这一点?

以下是我现在拥有的按钮的简单代码(仅处理“正常”的点击/触摸情况)。

Button(action: {self.BLEinfo.startScan() }) {
                        Text("Scan")
                    } .disabled(self.BLEinfo.isScanning)

我已经尝试添加“长按手势”,但它仍然只“执行”“正常/短”点击。这是我尝试的代码:

Button(action: {self.BLEinfo.startScan() }) {
                        Text("Scan")
                            .fontWeight(.regular)
                            .font(.body)
                        .gesture(
                            LongPressGesture(minimumDuration: 2)
                                .onEnded { _ in
                                    print("Pressed!")
                            }
                        )
                    }

谢谢!

杰拉德


我使用自定义的UIScrollView和自定义的UIView解决了这个问题:https://gist.github.com/danhalliday/79b003d1cdbb84069c5c9f24fe069827 - Dan Halliday
14个回答

1
也许我的方法能帮到你。在Xcode 15和iOS 17.1上测试过。ScrollView()没有被阻塞。
struct MultiTapButton<Label> : View where Label : View {
    
    let label: Label
    let singleTapAction: () -> Void
    let doubleTapAction: () -> Void
    let longPressAction: () -> Void
    let duration: Double
     
    init(singleTapAction: @escaping () -> Void, doubleTapAction: @escaping () -> Void, longPressAction: @escaping () -> Void, duration: Double, @ViewBuilder label: () -> Label) {
        self.label = label()
        self.singleTapAction = singleTapAction
        self.doubleTapAction = doubleTapAction
        self.longPressAction = longPressAction
        self.duration = duration
    }
    
    @GestureState private var onPressing: Bool = false
    @State var longAction: Bool = false
    
    var gesture: some Gesture {
            LongPressGesture(minimumDuration: duration)
                .updating($onPressing) { currentState, gestureState, transaction in
                    gestureState = currentState
                    transaction.animation = Animation.spring(duration: 0.0625)
                }
                .onEnded { finished in
                    longAction = true
                    longPressAction()
                }
        }
     
    var body: some View {
        Button(action: {
            if longAction {
                longAction = false
                return
            }
            singleTapAction()
        }) {
            label
        }
        .simultaneousGesture(gesture)
        .highPriorityGesture(
            TapGesture(count: 2)
                .onEnded { _ in
                    doubleTapAction()
                }
        )
    }
}

例子:

struct ExampleView: View {
    
    let gridItems = [GridItem(.fixed(120), spacing: 2), GridItem(.fixed(120), spacing: 2), GridItem(.fixed(120), spacing: 2)]
    
    var body: some View {
        ZStack(){
            Color.orange
            ScrollView(showsIndicators: true){
                LazyVGrid(columns: gridItems, alignment: .center, spacing: 2) {
                    ForEach(0..<24) { index in
                        MultiTapButton(singleTapAction: {
                            print("Single Tap Action")
                        }, doubleTapAction: {
                            print("Double Tap Action")
                        }, longPressAction: {
                            print("Long Press Action")
                        }, duration: 0.5) {
                            Rectangle()
                                .foregroundStyle(Color.purple)
                                .frame(width: 120, height: 150, alignment: .center)
                        }
                    }
                }
                .padding(.vertical, 64)
            }
        }
        .ignoresSafeArea()
    }
}

0

我想在这里回复一下,以防其他人也遇到同样的问题。奇怪的是,苹果的默认行为适用于大多数控件,但不适用于按钮。在我的情况下,我希望保留按钮效果同时支持长按。

一种不太复杂的方法是忽略默认的按钮操作,并创建一个同时处理正常和长按点击的手势。

在您的视图中,您可以应用自定义的长按修改器,如下所示:

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)
            })
    }
}

0

试试这个 :)

处理 isInactive、isPressing、isLongPress 和 Tap(Click)。

基于 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()
    }
}


0
!! 如果您需要按住手势才能弹出菜单,请考虑使用它(即使在Vision Pro上也能很好地工作)
Menu(content: {
    //The menu items you want to show when long press
    Button("Add", action: {})
    Button("Delete", action: {})
}, label: {
    Text("I am a item")
}, primaryAction: {
    //The tap gesture action
})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接