Convert the timeline's long press gesture recogniser to UIKit and prevent scroll view conflicts

This commit is contained in:
Stefan Ceriu
2025-10-11 18:50:09 +03:00
committed by Stefan Ceriu
parent b984c794fc
commit ba031c8472
2 changed files with 76 additions and 24 deletions
@@ -54,7 +54,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
content content
.offset(x: xOffset, y: 0.0) .offset(x: xOffset, y: 0.0)
.animation(.interactiveSpring().speed(0.5), value: xOffset) .animation(.interactiveSpring().speed(0.5), value: xOffset)
.gesture(PanGesture { gesture in .gesture(PanGestureRepresentable { gesture in
switch gesture.state { switch gesture.state {
case .ended, .cancelled, .failed: case .ended, .cancelled, .failed:
if xOffset > actionThreshold { if xOffset > actionThreshold {
@@ -204,7 +204,7 @@ struct SwipeRightAction_Previews: PreviewProvider, TestablePreview {
// Fixes the issue on iOS 18 where DragGesture conflicts with the scroll view // Fixes the issue on iOS 18 where DragGesture conflicts with the scroll view
// https://github.com/feedback-assistant/reports/issues/542#issuecomment-2581322968 // https://github.com/feedback-assistant/reports/issues/542#issuecomment-2581322968
private struct PanGesture: UIGestureRecognizerRepresentable { private struct PanGestureRepresentable: UIGestureRecognizerRepresentable {
var handle: (UIPanGestureRecognizer) -> Void var handle: (UIPanGestureRecognizer) -> Void
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() } func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }
@@ -15,20 +15,30 @@ struct LongPressWithFeedback: ViewModifier {
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
func body(content: Content) -> some View { func body(content: Content) -> some View {
content if #available(iOS 18, *) {
.compositingGroup() // Apply the shadow to the view as a whole. mainContent(content: content)
.shadow(color: .black.opacity(isLongPressing ? 0.2 : 0.0), radius: isLongPressing ? 12 : 0) .gesture(LongPressGestureRepresentable { gesture in
.shadow(color: .black.opacity(isLongPressing ? 0.1 : 0.0), radius: isLongPressing ? 3 : 0) switch gesture.state {
.scaleEffect(x: isLongPressing ? 1.05 : 1, case .ended, .cancelled, .failed:
y: isLongPressing ? 1.05 : 1) handleLongPress(isPressing: false)
.animation(.spring(response: 0.7).delay(isLongPressing ? 0.1 : 0).disabledDuringTests(), default:
value: isLongPressing) handleLongPress(isPressing: true)
// The minimum duration here doesn't actually invoke the perform block when elapsed (thus }
// the implementation below) but it does cancel other system gestures e.g. swipe to reply })
} else {
mainContent(content: content)
.onLongPressGesture(minimumDuration: 0.25) { } onPressingChanged: { isPressing in .onLongPressGesture(minimumDuration: 0.25) { } onPressingChanged: { isPressing in
handleLongPress(isPressing: isPressing)
}
}
}
// The gesture's minimum duration doesn't actually invoke the perform block when elapsed (thus
// the implementation below) but it does cancel other system gestures e.g. swipe to reply
private func handleLongPress(isPressing: Bool) {
isLongPressing = isPressing isLongPressing = isPressing
guard isPressing else { guard isLongPressing else {
triggerTask?.cancel() triggerTask?.cancel()
return return
} }
@@ -45,6 +55,17 @@ struct LongPressWithFeedback: ViewModifier {
feedbackGenerator.impactOccurred() feedbackGenerator.impactOccurred()
} }
} }
@ViewBuilder
private func mainContent(content: Content) -> some View {
content
.compositingGroup() // Apply the shadow to the view as a whole.
.shadow(color: .black.opacity(isLongPressing ? 0.2 : 0.0), radius: isLongPressing ? 12 : 0)
.shadow(color: .black.opacity(isLongPressing ? 0.1 : 0.0), radius: isLongPressing ? 3 : 0)
.scaleEffect(x: isLongPressing ? 1.05 : 1,
y: isLongPressing ? 1.05 : 1)
.animation(.spring(response: 0.7).delay(isLongPressing ? 0.1 : 0).disabledDuringTests(),
value: isLongPressing)
} }
} }
@@ -101,3 +122,34 @@ struct LongPressWithFeedback_Previews: PreviewProvider, TestablePreview {
} }
} }
} }
// Fixes the issue on iOS 18 where LongPress conflicts with the scroll view
// https://github.com/feedback-assistant/reports/issues/542#issuecomment-2581322968
private struct LongPressGestureRepresentable: UIGestureRecognizerRepresentable {
var handle: (UILongPressGestureRecognizer) -> Void
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }
func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
let gesture = UILongPressGestureRecognizer()
gesture.minimumPressDuration = 0.25
gesture.delegate = context.coordinator
gesture.isEnabled = true
return gesture
}
func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) {
handle(recognizer)
}
class Coordinator: NSObject, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
false
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
}
}