17

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)")
        }
    }
}
3
  • Does this answer your question stackoverflow.com/a/63833510/12299030? The idea is the same. Commented Sep 28, 2020 at 13:43
  • Any Update ?? If yes please post your ans. Commented Apr 13, 2021 at 9:18
  • This was helpful to me github.com/rick2785/Cart Commented Mar 22, 2022 at 12:08

2 Answers 2

25

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 OnDeleteModifier: 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)
                    .clipped()
                    .offset(x: geometry.size.width)
                    .onAppear {
                        contentWidth = geometry.size.width
                    }
                    .gesture(
                        TapGesture()
                            .onEnded {
                                delete()
                            }
                    )
                }
            )
            .offset(x: offset.width, y: 0)
            .simultaneousGesture(
                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(OnDeleteModifier(action: action))
    }
    
}
Sign up to request clarification or add additional context in comments.

7 Comments

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 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).
@Mina you could add .clipped() after the .frame(...) modifier to make the icon only visible once the frame is expanded in width.
Really great. Is it possible to reset the previous swiping after a new swiping?
@RobN I updated the code to use a simulatneousGesture, solving the scrolling issues. Also I added the "clipped()" to fix the problem with the icon.
|
5

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))
  }
}

4 Comments

Please edit your answer to highlight how it differs from the current accepted one.
.cornerRadius(radius:,corners:) is not a standard view modifier, you should include its implementation in your answer.
added remaining functions .cornerRadius(radius:,corners:)
There's a whitespace above the Rectangle in ZStack

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.