9

With a List one can simply use the .onDelete modifier to remove the rows from the List. But how can we do the same in a ForEach shown within a LazyVStack. I am using SwipeCell to apply a drag gesture and showing a delete button but how do I actually remove from CoreData, specifically how do I access the IndexSet in this case

LazyVStack {
    ForEach(items) { item in
        Text("\(item.name)")
            .swipeCell(cellPosition: .right, leftSlot: nil, rightSlot: slot1)
            .alert(isPresented: $showAlert){
                Alert(title: Text("Delete Task?"), message: nil, primaryButton:.destructive(Text("Delete"), action: {


                               // HOW DO I DELETE HERE ?


                               dismissDestructiveDelayButton()
                            }),secondaryButton: .cancel({dismissDestructiveDelayButton()}))
                        }
     }
}

private func deleteItems(offsets: IndexSet) {
    withAnimation {
        offsets.map { items[$0] }.forEach(viewContext.delete)
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
}
m.umar
  • 855
  • 4
  • 13
  • 23

2 Answers2

13

I've made a ViewModifier, that allows you to delete whatever view you want, with something similar to the Apple Animation.

When using, just call .onDelete { //perform deletion } on any View.

struct Delete: ViewModifier {
    
    let action: () -> Void
    
    @State var offset: CGSize = .zero
    @State var initialOffset: CGSize = .zero
    @State var contentWidth: CGFloat = 0.0
    @State var willDeleteIfReleased = false
   
    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { geometry in
                    ZStack {
                        Rectangle()
                            .foregroundColor(.red)
                        Image(systemName: "trash")
                            .foregroundColor(.white)
                            .font(.title2.bold())
                            .layoutPriority(-1)
                    }.frame(width: -offset.width)
                    .offset(x: geometry.size.width)
                    .onAppear {
                        contentWidth = geometry.size.width
                    }
                    .gesture(
                        TapGesture()
                            .onEnded {
                                delete()
                            }
                    )
                }
            )
            .offset(x: offset.width, y: 0)
            .gesture (
                DragGesture()
                    .onChanged { gesture in
                        if gesture.translation.width + initialOffset.width <= 0 {
                            self.offset.width = gesture.translation.width + initialOffset.width
                        }
                        if self.offset.width < -deletionDistance && !willDeleteIfReleased {
                            hapticFeedback()
                            willDeleteIfReleased.toggle()
                        } else if offset.width > -deletionDistance && willDeleteIfReleased {
                            hapticFeedback()
                            willDeleteIfReleased.toggle()
                        }
                    }
                    .onEnded { _ in
                        if offset.width < -deletionDistance {
                            delete()
                        } else if offset.width < -halfDeletionDistance {
                            offset.width = -tappableDeletionWidth
                            initialOffset.width = -tappableDeletionWidth
                        } else {
                            offset = .zero
                            initialOffset = .zero
                        }
                    }
            )
            .animation(.interactiveSpring())
    }
    
    private func delete() {
        offset.width = -contentWidth
        action()
    }
    
    private func hapticFeedback() {
        let generator = UIImpactFeedbackGenerator(style: .medium)
        generator.impactOccurred()
    }
    
    //MARK: Constants
    
    let deletionDistance = CGFloat(200)
    let halfDeletionDistance = CGFloat(50)
    let tappableDeletionWidth = CGFloat(100)
    
    
}

extension View {
    
    func onDelete(perform action: @escaping () -> Void) -> some View {
        self.modifier(Delete(action: action))
    }
    
}
lukbl
  • 169
  • 1
  • 4
  • I'm using this extension on a LazyVGrid with clear background on each row and you can always see the trash icon. Is there a way to toggle the opacity of it whenever you do a swipe action? So when you swipe left it appears and if you swipe right to cancel the deletion it will disappear? – Mina Jul 19 '21 at 21:34
  • @Mina maybe try putting the trash icon into an if statement and only show it, if the offset is less than 0 (or to be sure less than e.g. -5). – lukbl Oct 01 '21 at 09:02
  • @Mina call `.background(Color.systemBackground)` (or whatever color you are sing) before `.onDelete { //perform deletion }` – Lachtan Feb 08 '22 at 09:51
  • 1
    @Mina you could add .clipped() after the .frame(...) modifier to make the icon only visible once the frame is expanded in width. – Dominik Seemayr May 21 '22 at 19:33
  • 1
    Really great. Is it possible to reset the previous swiping after a new swiping? – aelam Aug 05 '22 at 16:27
1

I'm made some minor fixes, it works great. function edited delete().

import SwiftUI

struct Delete: ViewModifier {

    let action: () -> Void
    
    @State var offset: CGSize = .zero
    @State var initialOffset: CGSize = .zero
    @State var contentWidth: CGFloat = 0.0
    @State var willDeleteIfReleased = false
   
    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { geometry in
                    ZStack {
                        Rectangle()
                            .cornerRadius(radius: 7, corners: [.topRight, .bottomRight])
                            .foregroundColor(.red)
                        Image(systemName: "trash")
                            .foregroundColor(.white)
                            .font(.title2.bold())
                            .layoutPriority(-1)
                    }
                    .frame(width: -offset.width)
                    .clipShape(Rectangle() )
                    .offset(x: geometry.size.width)
                    .onAppear {
                        contentWidth = geometry.size.width
                    }
                    .gesture(
                        TapGesture()
                            .onEnded {
                                delete()
                            }
                    )
                }
            )
            .offset(x: offset.width, y: 0)
            .gesture (
                DragGesture()
                    .onChanged { gesture in
                        if gesture.translation.width + initialOffset.width <= 0 {
                            self.offset.width = gesture.translation.width + initialOffset.width
                        }
                        if self.offset.width < -deletionDistance && !willDeleteIfReleased {
                            hapticFeedback()
                            willDeleteIfReleased.toggle()
                        } else if offset.width > -deletionDistance && willDeleteIfReleased {
                            hapticFeedback()
                            willDeleteIfReleased.toggle()
                        }
                    }
                    .onEnded { _ in
                        if offset.width < -deletionDistance {
                            delete()
                        } else if offset.width < -halfDeletionDistance {
                            offset.width = -tappableDeletionWidth
                            initialOffset.width = -tappableDeletionWidth
                        } else {
                            offset = .zero
                            initialOffset = .zero
                        }
                    }
            )
            .animation(.interactiveSpring())
    }
    
    private func delete() {
        //offset.width = -contentWidth
        
        offset = .zero
        initialOffset = .zero
        action()
    }
    
    private func hapticFeedback() {
        let generator = UIImpactFeedbackGenerator(style: .medium)
        generator.impactOccurred()
    }
    
    //MARK: Constants
    
    let deletionDistance = CGFloat(200)
    let halfDeletionDistance = CGFloat(50)
    let tappableDeletionWidth = CGFloat(100)
    
    
}

extension View {
    
    func onDelete(perform action: @escaping () -> Void) -> some View {
        self.modifier(Delete(action: action))
    }
    
}
  • 1
    Please [edit](https://stackoverflow.com/posts/75841800/edit) your answer to highlight how it differs from the current accepted one. –  Mar 30 '23 at 22:07