mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
fix for swipe to reply on iOS 26 and also improved iOS 18 behaviour
This commit is contained in:
committed by
Stefan Ceriu
parent
699a42be95
commit
91b46aca33
@@ -28,10 +28,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
|||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
mainContent(content: content)
|
||||||
.offset(x: xOffset, y: 0.0)
|
|
||||||
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
|
||||||
.timelineGesture(gesture)
|
|
||||||
.onChange(of: dragGestureActive) { _, newValue in
|
.onChange(of: dragGestureActive) { _, newValue in
|
||||||
if newValue == true {
|
if newValue == true {
|
||||||
if shouldStartAction() {
|
if shouldStartAction() {
|
||||||
@@ -51,7 +48,65 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var gesture: some Gesture {
|
@ViewBuilder
|
||||||
|
private func mainContent(content: Content) -> some View {
|
||||||
|
if #available(iOS 18, *) {
|
||||||
|
content
|
||||||
|
.offset(x: xOffset, y: 0.0)
|
||||||
|
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
||||||
|
.gesture(PanGesture { gesture in
|
||||||
|
switch gesture.state {
|
||||||
|
case .ended, .cancelled, .failed:
|
||||||
|
if xOffset > actionThreshold {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
xOffset = 0.0
|
||||||
|
default:
|
||||||
|
guard shouldStartAction() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let translation = gesture.translation(in: nil)
|
||||||
|
|
||||||
|
// Due to https://forums.developer.apple.com/forums/thread/760035 we had to make
|
||||||
|
// the drag a simultaneous gesture otherwise it was impossible to scroll the timeline.
|
||||||
|
// Therefore we need to prevent the animation to run if the user is to scrolling vertically.
|
||||||
|
// It would be nice if we could somehow abort the gesture in this case.
|
||||||
|
let width: CGFloat = if translation.x > abs(translation.y) {
|
||||||
|
translation.x
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to add a spring like behaviour to the drag in which the view
|
||||||
|
// moves slower the more it's dragged. We use a circular easing function
|
||||||
|
// to generate those values up to the `swipeThreshold`
|
||||||
|
// The final translation will be between 0 and `swipeThreshold` with the action being enabled from
|
||||||
|
// `actionThreshold` onwards
|
||||||
|
let screenWidthNormalisedTranslation = max(0.0, min(width, swipeThreshold)) / swipeThreshold
|
||||||
|
let easedTranslation = circularEaseOut(screenWidthNormalisedTranslation)
|
||||||
|
xOffset = easedTranslation * xOffsetThreshold
|
||||||
|
|
||||||
|
if xOffset > actionThreshold {
|
||||||
|
if !hasReachedActionThreshold {
|
||||||
|
feedbackGenerator.impactOccurred()
|
||||||
|
hasReachedActionThreshold = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasReachedActionThreshold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
.offset(x: xOffset, y: 0.0)
|
||||||
|
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
||||||
|
.gesture(oldGesture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var oldGesture: some Gesture {
|
||||||
DragGesture()
|
DragGesture()
|
||||||
.updating($dragGestureActive) { _, state, _ in
|
.updating($dragGestureActive) { _, state, _ in
|
||||||
// Available actions should be computed on the fly so we use a gesture state change
|
// Available actions should be computed on the fly so we use a gesture state change
|
||||||
@@ -113,18 +168,6 @@ extension View {
|
|||||||
action: @escaping () -> Void) -> some View {
|
action: @escaping () -> Void) -> some View {
|
||||||
modifier(SwipeRightAction(label: label, shouldStartAction: shouldStartAction, action: action))
|
modifier(SwipeRightAction(label: label, shouldStartAction: shouldStartAction, action: action))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
fileprivate func timelineGesture(_ gesture: some Gesture) -> some View {
|
|
||||||
if #available(iOS 18.0, *) {
|
|
||||||
// iOS 18 has a bug https://forums.developer.apple.com/forums/thread/760035 and you
|
|
||||||
// can't scroll the timeline when `gesture` is used.
|
|
||||||
simultaneousGesture(gesture)
|
|
||||||
} else {
|
|
||||||
// Equally on iOS 17 you can't scroll the timeline when `simultaneousGesture` is used.
|
|
||||||
self.gesture(gesture)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SwipeRightAction_Previews: PreviewProvider, TestablePreview {
|
struct SwipeRightAction_Previews: PreviewProvider, TestablePreview {
|
||||||
@@ -159,3 +202,38 @@ 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 {
|
||||||
|
var handle: (UIPanGestureRecognizer) -> Void
|
||||||
|
|
||||||
|
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator { .init() }
|
||||||
|
|
||||||
|
func makeUIGestureRecognizer(context: Context) -> UIPanGestureRecognizer {
|
||||||
|
let gesture = UIPanGestureRecognizer()
|
||||||
|
gesture.delegate = context.coordinator
|
||||||
|
gesture.isEnabled = true
|
||||||
|
return gesture
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUIGestureRecognizerAction(_ recognizer: UIPanGestureRecognizer, context: Context) {
|
||||||
|
handle(recognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(
|
||||||
|
_ gestureRecognizer: UIGestureRecognizer,
|
||||||
|
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
||||||
|
) -> Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
guard let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false }
|
||||||
|
|
||||||
|
let velocity = panRecognizer.velocity(in: gestureRecognizer.view)
|
||||||
|
return abs(velocity.y) < abs(velocity.x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user