swiftswiftui

Gesture prevents list swipe action


I have a view where each list item has leading/trailing swipe actions. When I add a Drag gesture recognizer to the view the swipe actions become very hard to trigger. I want to add a drag gesture to the view to allow custom edge (left or right) swipes to navigate to a different view. I only really need this drag gesture to recognize on the left and right edges of the screen (this is what the gray colors represent). How can I make the swipe actions trigger normally while having the gesture for edge swipes?

import SwiftUI

struct swipeTest: View {
    @State var offset = 0.0
    var body: some View {
        VStack {
            List {
                ForEach(0..<60, id: \.self) { _ in
                    RoundedRectangle(cornerRadius: 10)
                        .frame(height: 50).foregroundStyle(.blue).padding(15)
                        .swipeActions(edge: .trailing) {
                            Button(action: {
                                
                            }, label: {
                                Image(systemName: "bell")
                            }).tint(.orange)
                        }
                        .swipeActions(edge: .leading) {
                            Button(action: {
                              
                            }, label: {
                                Image(systemName: "pin")
                            }).tint(.green)
                        }
                }
            }.listStyle(.plain)
        }
        .offset(x: offset)
        .overlay(alignment: .leading, content: {
            // Only need swipe on very left edge
            Color.gray.opacity(0.3).frame(width: 15)
        })
        .overlay(alignment: .trailing, content: {
            // Only need swipe on very right edge
            Color.gray.opacity(0.3).frame(width: 15)
        })
        .simultaneousGesture (
            DragGesture()
                .onChanged({ value in
                    let width = widthScreen()
                    
                    if value.startLocation.x < 15.0 || value.startLocation.x > (width - 15.0) {
                        offset = value.translation.width
                    }
                })
                .onEnded({ value in
                    offset = 0.0
                })
        )
    }
}

func widthScreen() -> CGFloat {
    let scenes = UIApplication.shared.connectedScenes
    let windowScene = scenes.first as? UIWindowScene
    let window = windowScene?.windows.first
    return window?.screen.bounds.width ?? 0
}


#Preview {
    swipeTest()
}

Solution

  • You're already added gray overlays to indicate where the custom drag gesture should activate, so just add the gesture to those Color.grays!

    Of course, in your real code you might want to use a Color.clear, to hide that region from the user. Here is an example:

    struct SwipeTest: View {
        @GestureState var offset = 0.0
        var body: some View {
            VStack {
                List {
                    ForEach(0..<60, id: \.self) { _ in
                        RoundedRectangle(cornerRadius: 10)
                            .frame(height: 50).foregroundStyle(.blue).padding(15)
                            .swipeActions(edge: .trailing) {
                                Button(action: {
                                    
                                }, label: {
                                    Image(systemName: "bell")
                                }).tint(.orange)
                            }
                            .swipeActions(edge: .leading) {
                                Button(action: {
                                  
                                }, label: {
                                    Image(systemName: "pin")
                                }).tint(.green)
                            }
                    }
                }.listStyle(.plain)
            }
            .offset(x: offset)
            .overlay(alignment: .leading) { dragRegion }
            .overlay(alignment: .trailing) { dragRegion }
            .animation(.default, value: offset)
        }
        
        var dragRegion: some View {
            let gesture = DragGesture().updating($offset) { value, state, transaction in
                state = value.translation.width
                transaction.disablesAnimations = true
            }
            return Color.clear
                .frame(width: 15)
                .contentShape(Rectangle())
                .gesture(gesture)
        }
    }
    

    Note that I have changed offset to a @GestureState, since your code resets it to 0 after the gesture ends. I have also taken the liberty to add an animation when the offset bounces back. In your real code, you said you wanted to "navigate to a different view", but it's unclear just from your question how exactly you want to do that, so I'll leave that part up to you.