iosswiftswiftuigesture

Swift UI detect start and end of gesture


I want to use a long press gesture and detect when the user has been holding for 1 consecutive second, and I also want to detect when the user lets go of the screen. Currently the onChanged detects when the gesture begins and the onEnded fires after 1 second. So I can use the onEnded to detect when the user has been holding for 1 consecutive second. But how can I know when the user lets go?

Color.blue
                .simultaneousGesture(LongPressGesture(minimumDuration: 1.0)
                    .onChanged { _ in
                        UIImpactFeedbackGenerator(style: .light).impactOccurred()
                    }
                    .onEnded { _ in
                        UIImpactFeedbackGenerator(style: .light).impactOccurred()
                    }
                )

Solution

  • As you have found out, LongPressGesture ends when the time interval required to trigger it has elapsed, instead of when the user has lifted their finger. Therefore, it is unsuitable for detecting the finger lifting.

    I would use a DragGesture instead. Its onChanged is called when the gesture starts, and its onEnded is called when the finger lifts. We can record the start time in onChanged, and end time in onEnded, and hence how long the user has been pressing down.

    @State var touchDownTime: Date?
    @State var impactTrigger = false
    
    var body: some View {
        Color.yellow
            .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onChanged({ value in
                        if touchDownTime == nil {
                            touchDownTime = value.time
                            impactTrigger.toggle()
                            print("Started")
                        }
                    })
                    .onEnded({ value in
                        if let touchDownTime,
                           value.time.timeIntervalSince(touchDownTime) >= 1 {
                            impactTrigger.toggle()
                            print("Ended")
                        }
                        self.touchDownTime = nil
                    })
            )
            .sensoryFeedback(.impact(weight: .light), trigger: impactTrigger)
    }
    

    Note that I have changed it to use sensoryFeedback to create the haptic feedback. If you are targeting an older version than iOS 17, using UIImpactFeedbackGenerator is fine too.

    Note that unlike a LongPressGesture, which doesn't trigger when the user moves their finger too much after pressing down, DragGesture is still recognised if the finger moves. If this is undesirable, use the value.translation property to determine whether the finger has moved too much.

    @State var shouldCancel = false
    ...
    .onChanged({ value in
        ...
        let threshold: CGFloat = 10 // decide a threshold
        if hypot(value.translation.width, value.translation.height) > threshold {
            shouldCancel = true
        }
    })
    .onEnded({ value in
        if let touchDownTime,
           !shouldCancel, // <----
           value.time.timeIntervalSince(touchDownTime) >= 1 {
            impactTrigger.toggle()
            print("Ended")
        }
        self.touchDownTime = nil
        shouldCancel = false
    })