Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
content
.offset(x: xOffset, y: 0.0)
.animation(.interactiveSpring().speed(0.5), value: xOffset)
.gesture(PanGesture { gesture in
.gesture(PanGestureRepresentable { gesture in
switch gesture.state {
case .ended, .cancelled, .failed:
if xOffset > actionThreshold {
Expand Down Expand Up @@ -204,7 +204,7 @@ struct SwipeRightAction_Previews: PreviewProvider, TestablePreview {

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

func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,49 @@ struct LongPressWithFeedback: ViewModifier {
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)

func body(content: Content) -> some View {
if #available(iOS 18, *) {
mainContent(content: content)
.gesture(LongPressGestureRepresentable { gesture in
switch gesture.state {
case .ended, .cancelled, .failed:
handleLongPress(isPressing: false)
default:
handleLongPress(isPressing: true)
}
})
} else {
mainContent(content: content)
.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

guard isLongPressing else {
triggerTask?.cancel()
return
}

feedbackGenerator.prepare()

triggerTask = Task {
// The wait time needs to be at least 0.5 seconds or the long press gesture will take precedence over long pressing links.
try? await Task.sleep(for: .seconds(0.5))

if Task.isCancelled { return }

action()
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)
Expand All @@ -23,28 +66,6 @@ struct LongPressWithFeedback: ViewModifier {
y: isLongPressing ? 1.05 : 1)
.animation(.spring(response: 0.7).delay(isLongPressing ? 0.1 : 0).disabledDuringTests(),
value: isLongPressing)
// 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
.onLongPressGesture(minimumDuration: 0.25) { } onPressingChanged: { isPressing in
isLongPressing = isPressing

guard isPressing else {
triggerTask?.cancel()
return
}

feedbackGenerator.prepare()

triggerTask = Task {
// The wait time needs to be at least 0.5 seconds or the long press gesture will take precedence over long pressing links.
try? await Task.sleep(for: .seconds(0.5))

if Task.isCancelled { return }

action()
feedbackGenerator.impactOccurred()
}
}
}
}

Expand Down Expand Up @@ -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
}
}
}
Loading