Nighthawk
Nighthawk

Reputation: 992

How to swipe to delete in SwiftUI with only a ForEach and NOT a List

I am making a custom list using a ForEach in SwiftUI. My goal is to make a swipe to delete gesture and not embed the ForEach into a List.

This is my code so far:

import SwiftUI

struct ContentView: View {
let list = ["item1", "item2", "item3", "item4", "item5", "item6"]

var body: some View {
    VStack {
        List{
            
            ForEach(list, id: \.self) { item in
                Text(item)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(20)
                    .padding()
                
            }
        }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I can't seem to find a gesture that will allow me to make a swipe to delete without using the list view.

I would also like to make a custom delete button what is shown when the user swipes an item to the left (like the below picture).

enter image description here

Upvotes: 14

Views: 10150

Answers (2)

Ihor Chernysh
Ihor Chernysh

Reputation: 496

Based on the existing solution:

  1. Added the same behaviour for swipe as it's in the List with animation.
  2. Provided comments in the code to clearly understand what's going on there.

You can update all constants at the top to your values and it will work well. And also can update .padding(.trailing, -40) to your values (to hide the delete button by default).

struct SomeContentView: View {

@State var list = ["item1", "item2", "item3", "item4", "item5", "item6"]
@State var offsets = [CGSize](repeating: CGSize.zero, count: 6)

private let swipeLeftLimit: CGFloat = -50
private let swipeRightLimit: CGFloat = 50
private let swipeLeftLimitToShow: CGFloat = -30
private let swipeRightLimitToHide: CGFloat = 30

var body: some View {
    VStack {
        ForEach(list.indices, id: \.self) { index in
            
            HStack {
            Text(list[index])
                .foregroundColor(.white)
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.red)
                .cornerRadius(20)
                .padding()
                
                Button(action: {
                    self.list.remove(at: index)
                    self.offsets.remove(at: index)
                }) {
                    Image(systemName: "xmark")
                }
            }
            .padding(.trailing, -40)
            .offset(x: offsets[index].width)
            .gesture(
                DragGesture()
                    .onChanged { gesture in
                        // Prevent swipe to the right in default position
                        if offsets[index].width == 0 && gesture.translation.width > 0 {
                            return
                        }
                        
                        // Prevent drag more than swipeLeftLimit points
                        if gesture.translation.width < swipeLeftLimit {
                            return
                        }
                        
                        // Prevent swipe againt to the left if it's already swiped
                        if offsets[index].width == swipeLeftLimit && gesture.translation.width < 0 {
                            return
                        }
                        
                        // If view already swiped to the left and we start dragging to the right
                        // Firstly will check if it's swiped left
                        if offsets[index].width >= swipeLeftLimit {
                            // And here checking if swiped to the right more than swipeRightLimit points
                            // If more - need to set the view to zero position
                            if gesture.translation.width > swipeRightLimit {
                                self.offsets[index] = .zero
                                return
                            }
                            
                            // Check if only swiping to the right - update distance by minus swipeLeftLimit points
                            if offsets[index].width != 0 && gesture.translation.width > 0 {
                                self.offsets[index] = .init(width: swipeLeftLimit + gesture.translation.width,
                                                            height: gesture.translation.height)
                                return
                            }
                        }
                            
                        self.offsets[index] = gesture.translation
                    }
                    .onEnded { gesture in
                        withAnimation {
                            // Left swipe handle:
                            if self.offsets[index].width < swipeLeftLimitToShow {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            if self.offsets[index].width < swipeLeftLimit {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            
                            // Right swipe handle:
                            if gesture.translation.width > swipeRightLimitToHide {
                                self.offsets[index] = .zero
                                return
                            }
                            if gesture.translation.width < swipeRightLimitToHide {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            
                            self.offsets[index] = .zero
                        }
                    }
            )
        }
    }
}

Also you can easily move 'onChange' to the separate method private func handleOnChangeGesture(_ gesture: DragGesture.Value, index: Int) {...paste code here...} And the same separate method for 'onEnd'.

Upvotes: 4

Ho Si Tuan
Ho Si Tuan

Reputation: 540

This is my solution for your problem. You should add DragGesture and create offset for each row. Keep in mind that your variable declare by var can't be mutated. You have to add @State before.

struct ContentView: View {
    @State var list = ["item1", "item2", "item3", "item4", "item5", "item6"]
    @State private var offsets = [CGSize](repeating: CGSize.zero, count: 6)
    var body: some View {
        VStack {
            ForEach(list.indices, id: \.self) { index in
                
                HStack {
                Text(list[index])
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(20)
                    .padding()
                    
                    Button(action: {
                        self.list.remove(at: index)
                        self.offsets.remove(at: index)
                    }) {
                        Image(systemName: "xmark")
                    }
                }
                .padding(.trailing, -40)
                .offset(x: offsets[index].width)
                .gesture(
                    DragGesture()
                        .onChanged { gesture in
                            self.offsets[index] = gesture.translation
                            if offsets[index].width > 50 {
                                self.offsets[index] = .zero
                            }
                        }
                        .onEnded { _ in
                            if self.offsets[index].width < -100 {
                                self.list.remove(at: index)
                                self.offsets.remove(at: index)
                            }
                            else if self.offsets[index].width < -50 {
                                self.offsets[index].width = -50
                            }
                        }
                )
            }
        }
    }
}

Upvotes: 4

Related Questions