当 TapGesture 也出现时,SwiftUI LongPressGesture 需要很长时间才能识别

问题描述 投票:0回答:5

我想识别同一商品上的

TapGesture
LongPressGesture
。它工作正常,但有以下例外:
LongPressGesture
在我指定的持续时间(0.25秒)后单独响应,但是当我将它与
TapGesture
结合使用时,它至少需要1秒——我找不到一种使其响应更快的方法。这是一个演示:

这是它的代码:

struct ContentView: View {
    @State var message = ""

    var body: some View {
        Circle()
            .fill(Color.yellow)
            .frame(width: 150, height: 150)
            .onTapGesture(count: 1) {
                message = "TAP"
            }
            .onLongPressGesture(minimumDuration: 0.25) {
                message = "LONG\nPRESS"
            }
            .overlay(Text(message)
                        .font(.title).bold()
                        .multilineTextAlignment(.center)
                        .allowsHitTesting(false))
    }
}

请注意,除了长按的持续时间(远长于 0.25 秒)之外,它工作正常。

有什么想法吗?预先感谢!

ios swiftui gesture duration long-press
5个回答
2
投票

为了拥有一些适合项目中每个人需求的多手势,Apple 没有提供比普通手势更好的功能,将它们混合在一起以达到所需的手势有时会很棘手,这是一个救赎,工作没有问题或错误!

这里有一个自定义的零问题手势,称为interactionReader,我们可以将其应用到任何视图。 同时拥有LongPressGestureTapGesture



import SwiftUI

struct ContentView: View {
    
    var body: some View {
        
        Circle()
            .fill(Color.yellow)
            .frame(width: 150, height: 150)
            .interactionReader(longPressSensitivity: 250, tapAction: tapAction, longPressAction: longPressAction, scaleEffect: true)
            .animation(Animation.easeInOut(duration: 0.2))
        
    }
    
    func tapAction() { print("tap action!") }
    
    func longPressAction() { print("longPress action!") }
    
}

struct InteractionReaderViewModifier: ViewModifier {
    
    var longPressSensitivity: Int
    var tapAction: () -> Void
    var longPressAction: () -> Void
    var scaleEffect: Bool = true
    
    @State private var isPressing: Bool = Bool()
    @State private var currentDismissId: DispatchTime = DispatchTime.now()
    @State private var lastInteractionKind: String = String()
    
    func body(content: Content) -> some View {
        
        let processedContent = content
            .gesture(gesture)
            .onChange(of: isPressing) { newValue in
                
                currentDismissId = DispatchTime.now() + .milliseconds(longPressSensitivity)
                let dismissId: DispatchTime = currentDismissId
                
                if isPressing {
                    
                    DispatchQueue.main.asyncAfter(deadline: dismissId) {
                        
                        if isPressing { if (dismissId == currentDismissId) { lastInteractionKind = "longPress"; longPressAction() } }
                        
                    }
                    
                }
                else {
                    
                    if (lastInteractionKind != "longPress") { lastInteractionKind = "tap"; tapAction() }
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {lastInteractionKind = "none"}
                    
                    
                }
                
            }
        
        return Group {
            
            if scaleEffect { processedContent.scaleEffect(lastInteractionKind == "longPress" ? 1.5: (lastInteractionKind == "tap" ? 0.8 : 1.0 )) }
            else { processedContent }
            
        }

    }
    
    var gesture: some Gesture {
        
        DragGesture(minimumDistance: 0.0, coordinateSpace: .local)
            .onChanged() { _ in if !isPressing { isPressing = true } }
            .onEnded() { _ in isPressing = false }
        
    }
    
}

extension View {
    
    func interactionReader(longPressSensitivity: Int, tapAction: @escaping () -> Void, longPressAction: @escaping () -> Void, scaleEffect: Bool = true) -> some View {
        
        return self.modifier(InteractionReaderViewModifier(longPressSensitivity: longPressSensitivity, tapAction: tapAction, longPressAction: longPressAction, scaleEffect: scaleEffect))
        
    }
    
}

2
投票

发现这个老问题,因为我只需要做类似的事情,我想我会分享。我最终得到了这段代码,只要它不附加到按钮(它吸收一些手势),它似乎就可以很好地工作。将以下代码添加到图像或圆形或其他类似内容中,它将立即检测到轻击,或在 0.25 秒后检测到长按。 (请注意,这只会检测其中之一,而不是两者。)

      .onTapGesture(count: 1) {
        print("Tap Gesture. \(Date().timeIntervalSince1970)")
      }
      .simultaneousGesture(
        LongPressGesture(minimumDuration: 0.25)
          .onEnded() { value in
            print("LongPressGesture started. \(Date().timeIntervalSince1970)")
          }
          .sequenced(before:TapGesture(count: 1)
            .onEnded {
              print("LongPressGesture ended. \(Date().timeIntervalSince1970)")
            }))

我认为您之前问题的根源是点击手势必须“下降”到长按手势,这需要一点时间。这两个同时启动,但只有一个成功。


0
投票

LongPressGesture 还有另一个完成功能,可以在用户第一次触摸时执行操作,并在最短的持续时间内再次执行操作。像这样的东西可以工作吗?

struct LongTapTest: View {
    @State var message = ""

    var body: some View {
        Circle()
            .fill(Color.yellow)
            .frame(width: 150, height: 150)
            .onLongPressGesture(minimumDuration: 0.25, maximumDistance: 50, pressing: { (isPressing) in
            if isPressing {
                // called on touch down
            } else {
                // called on touch up
                message = "TAP"
            }
        }, perform: {
            message = "LONG\nPRESS"
        })
            .overlay(Text(message)
                        .font(.title).bold()
                        .multilineTextAlignment(.center)
                        .allowsHitTesting(false))
    }
}

0
投票

嗯,这并不漂亮,但效果很好。它记录每次点击/按下的开始,如果它在 0.25 秒之前结束,则将其视为

TapGesture
,否则将其视为
LongPressGesture
:

struct ContentView: View {
    @State var pressInProgress = false
    @State var gestureEnded = false
    @State var workItem: DispatchWorkItem? = nil
    @State var message = ""

    var body: some View {

        Circle()
            .fill(Color.yellow)
            .frame(width: 150, height: 150, alignment: .center)
            .overlay(Text(message)
                         .font(.title).bold()
                         .multilineTextAlignment(.center)
                         .allowsHitTesting(false))
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .global)
                    .onChanged { _ in
                        guard !pressInProgress else { return }
                        pressInProgress = true
                        workItem = DispatchWorkItem {
                            message = "LONG\nPRESSED"
                            gestureEnded = true
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: workItem!)
                }
                .onEnded { _ in
                    pressInProgress = false
                    workItem?.cancel()
                    guard !gestureEnded else { // moot if we're past 0.25 seconds so ignore
                        gestureEnded = false
                        return
                    }
                    message = "TAPPED"
                    gestureEnded = false
                }
        )
    }
}

如果有人能想到一个实际使用

LongPressGesture
TapGesture
的解决方案,我会更喜欢!


0
投票

我对延误感到恼火。肯定有适合给定问题的答案。但是,我遇到了一个问题,因为我的视图也在 ScrollView(或 UITableView)内,因此使用

simultaneousGesture
不是一个选项,因为我失去了滚动视图的能力。

但是在 @nicksarno 答案的基础上,我创建了一个自定义修饰符,并利用

onPressingChanged
闭包和自定义延迟。这样我就可以在调用本机
perform:
闭包之前触发长按操作。此外,通过这种方法,您可以添加视觉反馈(在我的例子中 - 长按时缩放视图)。很想听听你的想法。

使用方法很简单,只需将onLongPress替换为自定义修饰符即可:

Circle()
    .fill(Color.yellow)
    .frame(width: 150, height: 150)
    .onTapGesture(count: 1) {
        message = "TAP"
    }
    .onScalingLongPress {
        message = "LONG\nPRESS"
    }

实施:

import SwiftUI

extension View {
    func onScalingLongPress(perform action: @escaping () -> Void) -> some View {
        modifier(ScalingLongPressModifier(action: action))
    }
}

struct ScalingLongPressModifier: ViewModifier {
    @State private var longPressTask: Task<Void, Never>?
    @State private var shouldScale: Bool = false
    var scaleWhenPressed: Double = 0.975
    var action: VoidCompletion
    
    func body(content: Content) -> some View {
        content
            .scaleEffect(shouldScale ? scaleWhenPressed : 1.0)
            .onLongPressGesture(
                minimumDuration: 0.2,
                maximumDistance: 50,
                perform: {
                    // do nothing
                },
                onPressingChanged: { isPressing in
                    handlePressingChange(isPressing: isPressing)
                })
    }
    
    @MainActor
    private func handlePressingChange(isPressing: Bool) {
        if isPressing {
            longPressTask = Task {
                // Wait and scale the view
                try? await Task.sleep(nanoseconds: 200_000_000)
                
                guard !Task.isCancelled else {
                    return
                }
                
                withAnimation(.spring()) {
                    shouldScale = true
                }
                
                // Wait and trigger the action
                try? await Task.sleep(nanoseconds: 200_000_000)
                
                guard !Task.isCancelled else {
                    return
                }
                
                action()
            }
        } else {
            longPressTask?.cancel()
            longPressTask = nil
            
            withAnimation(.spring()) {
                shouldScale = false
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.