ios: SwiftUI LongPressGesture tarda demasiado en reconocer cuando TapGesture también está presente

CorePress2024-01-25  257

Me gustaría reconocer TapGesture y LongPressGesture en el mismo elemento. Y funciona bien, con la siguiente excepción: LongPressGesture por sí solo responde después de la duración que especifico, que es 0,25 segundos, pero cuando lo combino con TapGesture, tarda al menos 1 segundo; no puedo encontrar una manera de hacerlo. responde más rápidamente. Aquí hay una demostración:

Y aquí está el código:

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

Observa que funciona bien excepto durante la duración de LongPress, que es mucho más larga que 0,25 segundos.

¿Alguna idea? ¡Gracias de antemano!



------------------------------------

Encontré esta vieja pregunta y desde entoncese Simplemente tenía que hacer algo similar, pensé en compartirlo. Terminé con este código, que parece funcionar bastante bien siempre y cuando no esté adjunto a un Botón (que absorbe algunos de los gestos). Agregue el siguiente código a una imagen, un círculo o cualquier otra cosa, y detectará un toque inmediatamente o una pulsación larga después de 0,25 segundos. (Tenga en cuenta que esto solo detectará uno u otro, no ambos).

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

Creo que la fuente de tu problema anterior era que el gesto de tocar tenía que "fallar" al gesto de presión prolongada, que tomó un poco de tiempo. Éste inicia ambos simultáneamente, pero solo uno lo logra.

------------------------------

Para tener algún gesto múltiple que se adapte a las necesidades de cada uno en los proyectos, Apple no tiene otra oferta que el gesto normal, mezclarlos para alcanzar el gesto deseado a veces resulta complicado, ¡aquí hay una salvación, trabajar sin problemas ni errores!

Aquí hay un gesto personalizado sin problemas llamado interacciónReader, podemos aplicarlo a cualquier Vista. por tener LongPressGesture y TapGesture al mismo tiempo.

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

7

Gracias, @nicksarno. Me parece que eso se dispararía una vez.e en la inicialización del toque/prensa y luego nuevamente cuando/si la pulsación dura 0,25 segundos. Es una buena línea a seguir, pero no resuelve el problema; lo que se necesitaría es que el primero registre que se ha producido un evento de toque/prensa, y luego decida si es un toque o una pulsación larga dependiendo de cuándo termina la prensa. Si pasan menos de 0,25 segundos después de comenzar, entonces es un toque; de lo contrario, es una pulsación larga. De hecho, eso me da una idea para una solución que publicaré en breve. ¡Gracias!

- Antón

29 de marzo de 2021 a las 0:51

@Anton he editado miResponda a lo que creo que podría funcionar para usted. IsPressing se llama dos veces, una cuando el usuario toca por primera vez y otra vez cuando el usuario levanta el dedo. Podrías hacer que la lógica para "TAP" ejecutar cuando el dedo se levanta (cuando isPressing es falso).

-nicksarno

29 de marzo de 2021 a las 2:10

Ah, eso es fantástico. Nunca noté el cierre apremiante. Con el cambio que hiciste, funciona muy bien. ¡Gracias, @nicksarno!

- Antón

29 de marzo de 2021 a las 3:32

1

Por supuesto, Nick. ¡Solo quería probarlo primero para asegurarme de que no me faltaba nada! :)

- Antón

29 de marzo de 2021 a las 3:51

1

Desafortunadamente lo probé y tampoco funciona. Si se detecta una pulsación larga exitosa, tanto el cierre de presión como el de ejecución se activan. Se puede arreglar agregando una variable de estadoble, sin embargo.

- Antón

31 de marzo de 2021 a las 5:13



------------------------------------

Bueno, esto no es bonito, pero funciona bien. Registra el comienzo de cada toque/presión y si finaliza antes de 0,25 segundos lo considera un TapGesture; de ​​lo contrario, lo considera un 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
                }
        )
    }
}

Si alguien puede pensar en una solución que realmente utilice LongPressGesture y TapGesture, ¡la preferiría!

2

dijiste que si alguien puede pensar en una solución que realmente use LongPressGesture y TapGesture, ¡preferiría eso!, de hecho, todos provienen de DragGesture, Apple creó ese simple TapGesture a partir de DragGesture, parece una locura. pero eso es lo que es. y cuando los mezclas, hay un problema, recuerda que te dije que deberíamos trabajar en un gesto personalizado, en mi respuesta a tu pregunta, básicamente hice un gesto nuevo. por eso funciona como queríamos. Y le puse el nombre de Lector de interacción de gestos, puedes aplicar Lector de interacción a cualquier Vista, funcionaría para tap/LongPressGesture

- codificador ios

3 de abril de 2021 a las 17:35

Gracias por tu solución anterior, swiftPunk. ¡Muchas gracias! Para ser claros, lo que busco no es un gesto personalizado. Más bien, es una implementación de los gestos estándar de Apple, de modo que buscar varios no genera errores. En general, los gestos de Apple se pueden componer, y componerlos funciona bien siempre que el parámetro LongPressGesture sea de 1 segundo o más... pero no funciona para especificar si a LongPressGesture se le da una duración más corta.

- Antón

4 de abril de 2021 a las 16:00



------------------------------------

Me molestó el retraso. Definitivamente hay respuestas que funcionan para la pregunta dada. Sin embargo, tuve un problema ya que mis vistas también estaban dentro de ScrollView (o UITableView), por lo que usar simultáneoGesture no era una opción ya que perdí la capacidad de desplazarme por la vista.

Pero basándose en la respuesta de @nicksarno, creé un modificador personalizado y aproveché el cierre onPressingChanged con el retraso personalizado. De esta manera puedo activar la acción de pulsación larga antes de que se llame a la ejecución nativa: cierre. Además, con este enfoque, puede agregar comentarios visuales (en mi caso, escalar la vista al presionar prolongadamente). Tengo curiosidad por escuchar tu opinión.

El uso es simple, simplemente reemplace onLongPress con el modificador personalizado:

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

La implementación:

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: () -> Void
    
    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
            }
        }
    }
}

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare