Deleting rows inside LazyVStack and ForEach in SwiftUI
Asked Answered
N

2

15

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)")
        }
    }
}
Numbskull answered 28/9, 2020 at 13:25 Comment(3)
Does this answer your question https://mcmap.net/q/823396/-how-to-create-a-custom-delete-button-without-using-the-slide-to-delete-that-comes-with-swiftui-i-am-not-using-list-just-using-a-foreach-loop? The idea is the same.Nomology
Any Update ?? If yes please post your ans.Steele
This was helpful to me github.com/rick2785/CartSphere
C
22

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))
    }
    
}
Canadianism answered 2/5, 2021 at 13:47 Comment(5)
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?Muff
@Muff 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).Canadianism
@Muff call .background(Color.systemBackground) (or whatever color you are sing) before .onDelete { //perform deletion }Mope
@Muff you could add .clipped() after the .frame(...) modifier to make the icon only visible once the frame is expanded in width.Teodor
Really great. Is it possible to reset the previous swiping after a new swiping?Omega
D
4

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))
    }
    func cornerRadius(radius: CGFloat, corners: UIRectCorner) -> some View {
        ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners))
    }
}

struct CornerRadiusShape: Shape {
   var radius = CGFloat.infinity
   var corners = UIRectCorner.allCorners

   func path(in rect: CGRect) -> Path {
     let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
    return Path(path.cgPath)
   }
}

struct CornerRadiusStyle: ViewModifier {
   var radius: CGFloat
   var corners: UIRectCorner

   func body(content: Content) -> some View {
     content
        .clipShape(CornerRadiusShape(radius: radius, corners: corners))
  }
}
Decern answered 25/3, 2023 at 12:49 Comment(4)
Please edit your answer to highlight how it differs from the current accepted one.Regenerate
.cornerRadius(radius:,corners:) is not a standard view modifier, you should include its implementation in your answer.Nitrate
added remaining functions .cornerRadius(radius:,corners:)Decern
There's a whitespace above the Rectangle in ZStackLassie

© 2022 - 2024 — McMap. All rights reserved.