swiftswiftuiswiftui-listtaphstack

How to limit the tappable area in HStack to make just the delete button tappable


In my ListView, There is a HStack consisting of checkbox, Text, Spacer and a Delete Button. Unfortunately, clicking at any part of the HStack (Checkbox/Text/Spacer area) causing the delete button action firing. How to limit this delete action only applicable for delete button, not the whole HStack view in SwiftUI ListView?

extension View {
    @ViewBuilder public func hidden(_ shouldHide: Bool) -> some View {
        switch shouldHide {
        case true: self.hidden()
        case false: self
        }
    }
}

struct AgendaItem: Identifiable {
    let id = UUID()
    var title: String
    var isSelected = false
}

struct AgendaList: View {
    @State private var agendaItems = [AgendaItem]()
    @State private var newAgendaItem = ""
    
    var body: some View {
        VStack {
            Text("Agenda List")
                .font(.title)
            
            HStack {
                ZStack(alignment: .topLeading) {
                    TextEditor(text: $newAgendaItem)
                        .frame(height: 40)
                        .overlay(
                            RoundedRectangle(cornerRadius: 3)
                                .stroke(Color(UIColor.opaqueSeparator), lineWidth: 0.5)
                        )
                    Text("Add agenda")
                        .fontWeight(.light)
                        .foregroundColor(.black.opacity(0.25))
                        .padding(8)
                        .hidden(!newAgendaItem.isEmpty)
                }
                
                Button(action: addItem) {
                    Text("Add")
                }
                .padding(.horizontal)
            }
            .padding()
            
            List(agendaItems) { agendaItem in
                HStack(alignment: .top, spacing: 8) {
                    VStack {
                        CheckBox(isChecked: $agendaItems[getIndex(for: agendaItem.id)].isSelected)
                            .padding(.top, 3)
                        Spacer()
                    }
                    Text(agendaItem.title)
                        .strikethrough(agendaItem.isSelected)
                    Spacer()
                    Button(action: {
                        deleteItem(at: getIndex(for: agendaItem.id))
                    }) {
                        Image(systemName: "trash")
                    }
                }
            }
            .listRowBackground(Color.red)
        }
    }
    
    func addItem() {
        agendaItems.append(AgendaItem(title: newAgendaItem))
        newAgendaItem = ""
    }
    
    func getIndex(for id: UUID) -> Int {
        return agendaItems.firstIndex { $0.id == id } ?? 0
    }
    
    func deleteItem(at index: Int) {
        agendaItems.remove(at: index)
    }
}

struct CheckBox: View {
    @Binding var isChecked: Bool
    
    var body: some View {
        Button(action: {
            isChecked.toggle()
        }) {
            Image(systemName: isChecked ? "checkmark.square.fill" : "square")
                .foregroundColor(isChecked ? .blue : .gray)
        }
    }
}

Solution

  • You can achieve this by adding below two properties for button:

    Button(action: {
        deleteItem(at: getIndex(for: agendaItem.id))
    }) {
        Image(systemName: "trash")
    }
    .onTapGesture { }
    .buttonStyle(BorderlessButtonStyle())
    

    The buttonStyle property of the Button view is set to BorderlessButtonStyle to remove the button's background. The onTapGesture modifier is added to the Button view to handle the tap event, and an empty closure is provided to the onTapGesture modifier to prevent the list row from being tappable.