Skip to content

Commit 08fe97a

Browse files
committed
Convert the timeline's long press gesture recogniser to UIKit and prevent scroll view conflicts
1 parent 4273044 commit 08fe97a

File tree

2 files changed

+76
-24
lines changed

2 files changed

+76
-24
lines changed

ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
5454
content
5555
.offset(x: xOffset, y: 0.0)
5656
.animation(.interactiveSpring().speed(0.5), value: xOffset)
57-
.gesture(PanGesture { gesture in
57+
.gesture(PanGestureRepresentable { gesture in
5858
switch gesture.state {
5959
case .ended, .cancelled, .failed:
6060
if xOffset > actionThreshold {
@@ -204,7 +204,7 @@ struct SwipeRightAction_Previews: PreviewProvider, TestablePreview {
204204

205205
// Fixes the issue on iOS 18 where DragGesture conflicts with the scroll view
206206
// https://github.com/feedback-assistant/reports/issues/542#issuecomment-2581322968
207-
private struct PanGesture: UIGestureRecognizerRepresentable {
207+
private struct PanGestureRepresentable: UIGestureRecognizerRepresentable {
208208
var handle: (UIPanGestureRecognizer) -> Void
209209

210210
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }

ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,49 @@ struct LongPressWithFeedback: ViewModifier {
1515
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
1616

1717
func body(content: Content) -> some View {
18+
if #available(iOS 18, *) {
19+
mainContent(content: content)
20+
.gesture(LongPressGestureRepresentable { gesture in
21+
switch gesture.state {
22+
case .ended, .cancelled, .failed:
23+
handleLongPress(isPressing: false)
24+
default:
25+
handleLongPress(isPressing: true)
26+
}
27+
})
28+
} else {
29+
mainContent(content: content)
30+
.onLongPressGesture(minimumDuration: 0.25) { } onPressingChanged: { isPressing in
31+
handleLongPress(isPressing: isPressing)
32+
}
33+
}
34+
}
35+
36+
// The gesture's minimum duration doesn't actually invoke the perform block when elapsed (thus
37+
// the implementation below) but it does cancel other system gestures e.g. swipe to reply
38+
private func handleLongPress(isPressing: Bool) {
39+
isLongPressing = isPressing
40+
41+
guard isLongPressing else {
42+
triggerTask?.cancel()
43+
return
44+
}
45+
46+
feedbackGenerator.prepare()
47+
48+
triggerTask = Task {
49+
// The wait time needs to be at least 0.5 seconds or the long press gesture will take precedence over long pressing links.
50+
try? await Task.sleep(for: .seconds(0.5))
51+
52+
if Task.isCancelled { return }
53+
54+
action()
55+
feedbackGenerator.impactOccurred()
56+
}
57+
}
58+
59+
@ViewBuilder
60+
private func mainContent(content: Content) -> some View {
1861
content
1962
.compositingGroup() // Apply the shadow to the view as a whole.
2063
.shadow(color: .black.opacity(isLongPressing ? 0.2 : 0.0), radius: isLongPressing ? 12 : 0)
@@ -23,28 +66,6 @@ struct LongPressWithFeedback: ViewModifier {
2366
y: isLongPressing ? 1.05 : 1)
2467
.animation(.spring(response: 0.7).delay(isLongPressing ? 0.1 : 0).disabledDuringTests(),
2568
value: isLongPressing)
26-
// The minimum duration here doesn't actually invoke the perform block when elapsed (thus
27-
// the implementation below) but it does cancel other system gestures e.g. swipe to reply
28-
.onLongPressGesture(minimumDuration: 0.25) { } onPressingChanged: { isPressing in
29-
isLongPressing = isPressing
30-
31-
guard isPressing else {
32-
triggerTask?.cancel()
33-
return
34-
}
35-
36-
feedbackGenerator.prepare()
37-
38-
triggerTask = Task {
39-
// The wait time needs to be at least 0.5 seconds or the long press gesture will take precedence over long pressing links.
40-
try? await Task.sleep(for: .seconds(0.5))
41-
42-
if Task.isCancelled { return }
43-
44-
action()
45-
feedbackGenerator.impactOccurred()
46-
}
47-
}
4869
}
4970
}
5071

@@ -101,3 +122,34 @@ struct LongPressWithFeedback_Previews: PreviewProvider, TestablePreview {
101122
}
102123
}
103124
}
125+
126+
// Fixes the issue on iOS 18 where LongPress conflicts with the scroll view
127+
// https://github.com/feedback-assistant/reports/issues/542#issuecomment-2581322968
128+
private struct LongPressGestureRepresentable: UIGestureRecognizerRepresentable {
129+
var handle: (UILongPressGestureRecognizer) -> Void
130+
131+
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }
132+
133+
func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
134+
let gesture = UILongPressGestureRecognizer()
135+
gesture.minimumPressDuration = 0.25
136+
gesture.delegate = context.coordinator
137+
gesture.isEnabled = true
138+
return gesture
139+
}
140+
141+
func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) {
142+
handle(recognizer)
143+
}
144+
145+
class Coordinator: NSObject, UIGestureRecognizerDelegate {
146+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
147+
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
148+
false
149+
}
150+
151+
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
152+
true
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)