.onTapGesture problems

84 Views Asked by At

I have StaticTaskListView which uses a TaskView to display a filtered array of tasks. I use the StaticTaskListView for the "Done" and "Upcoming" sections of my Task Management app for macOS, and they display the tasks that are checked (have the isCompleted value set to true) or unchecked (the opposite).

The TaskView:

struct TaskView: View {
    @Binding var tasks: [Task]
    @Binding var task: Task
    @Binding var isStrikethrough: Bool
    @State private var isEditingDueDate = false

    var body: some View {
        HStack {
            Image(systemName: task.isCompleted ? "checkmark.square.fill" : "stop")
                .onTapGesture {
                    withAnimation {
                        task.isCompleted.toggle()
                        print("task.isCompleted: \(task.isCompleted)")
                    }
                }
            
            VStack(alignment: .leading) {
                if isStrikethrough {
                    Text(task.title)
                        .fixedSize(horizontal: false, vertical: true)
                        .strikethrough(task.isCompleted)
                        .onTapGesture {
                            isStrikethrough.toggle()
                        }
                } else {
                    TextField("New Task", text: $task.title, axis: .vertical)
                        .fixedSize(horizontal: false, vertical: true)
                        .textFieldStyle(.plain)
                        .strikethrough(task.isCompleted)
                }
            }

            Spacer()
            Text("\(dueDateFormatted)")
                .foregroundColor(.gray)
                .font(.caption)

            Button(action: {
                // Action for showing popover for editing due date
                isEditingDueDate.toggle()
            }) {
                Image(systemName: "calendar")
            }
            .popover(isPresented: $isEditingDueDate) {
                CalendarPopover(task: $task, isEditingDueDate: $isEditingDueDate)
                    .frame(width: 250, height: 50) // Set popover size as needed
            }
            .buttonStyle(PlainButtonStyle())
        }
    }

    private var dueDateFormatted: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .short
        dateFormatter.timeStyle = .short
        return dateFormatter.string(from: task.dueDate)
    }
}

As can be seen in the above code, the checkbox icon(Image(systemName: task.isCompleted ? "checkmark.square.fill" : "stop") toggles the isCompleted value of a task.

In my StaticTaskListView, I access the TaskView directly, yet for some reason the checkbox icon is not reacting to tap gestures and updating the isCompleted value accordingly, despite this function being controlled by the TaskView. Here is this StaticTaskListview:

struct StaticTaskListView: View {
    let title: String
    let tasks: [Task]
    let toggleAction: (Task) -> Void
    let deleteAction: (Task) -> Void
    let isStrikethrough: Bool

    var body: some View {
        List {
            ForEach(tasks) { task in
                TaskView(tasks: .constant([task]), task: .constant(task), isStrikethrough: .constant(isStrikethrough))
            }
            .onDelete { indexSet in
                if let firstIndex = indexSet.first {
                    let taskToDelete = tasks[firstIndex]
                    deleteAction(taskToDelete)
                }
            }
        }
    }
}

FOR CONTEXT

What my app looks like ("All" view):

On the sidebar, you can see the "Done" and "Upcoming" sections, which are the ones that use StaticTaskListView to display a filtered array of tasks.

What the "Done" section looks like:

enter image description here

The TaskListView, which is used for the "all" section and works fine (toggles isCompleted fine): is here for reference. It also uses the TaskView directly:

struct TaskListView: View {
    
    @Environment(\.colorScheme) private var colorScheme
    var onDelete: (Task) -> Void
    
    let title: String
    @Binding var tasks: [Task]
    @Binding var isStrikethrough: Bool
    
    var body: some View {
            List {
                ForEach($tasks) { $task in
                    TaskView(tasks: $tasks, task: $task, isStrikethrough: $isStrikethrough)
                        .contextMenu {
                            Button(action: {
                                // Handle task deletion here
                                deleteTask(task: task)
                            }) {
                                Text("Delete")
                                Image(systemName: "trash")
                            }
                        }
                }
                .onDelete { indexSet in
                    if let firstIndex = indexSet.first {
                        // Assume that indexSet has only one element for simplicity
                        let taskToDelete = tasks[firstIndex]
                        deleteTask(task: taskToDelete)
                    }
                }
            }
        }


        private func deleteTask(task: Task) {
            // Adjust the logic to delete the task
            if let index = tasks.firstIndex(where: { $0.id == task.id }) {
                tasks.remove(at: index)
            }
        }
    }

THE DESIRED FUNCTIONALITY

Below is the desired functionality, shown in a previous version of my app where this functionality was not broken: enter image description here

IMPORTANT NOTE The functionality seen in the GIF for the task disappearing and the status being updated globally is already handled in my app, hence it working in the previous version. The only thing that isn't working in my current version is that the checkbox is not reacting to clicks. In this previous version, I did not use TaskView directly in the StaticTaskListView and instead recreated all the TaskView elements in the StaticTaskListView, practically duplicating the file. This got tedious to update, hence using an instance of TaskView directly in StaticTaskListView in the current version.

THE PROBLEM, SUMMARISED Essentially, the only thing that needs to change, and the reason for this post, is fixing the unresponsive checkbox button, allowing it to react to clicks. If anyone can find the source of the problem, that's all I need.

I am new to Swift and to StackOverflow. I understand that my code likely doesn't make too much sense in some places, or that I misuse some features of SwiftUI. If any further clarification or context is required I will provide it.

1

There are 1 best solutions below

1
dktaylor On

The main problem is that you are not actually handling the state of each Task. A @Binding property will be observed by SwiftUI to carry out view changes as needed, but it isn't responsible for holding on to that state, which is the job of an @State property (among others, such as @ObservedObject). You're creating Binding<Task> objects when you declare the TaskView, whereas you should be using the projected value of some state property.

I suspect further up in your view hierarchy you do have some stateful property, and that's why your existing TaskListView works. Those @Binding properties are observing state changes above.

This overview of state management in SwiftUI is worth a read.

The main change you need to make is in StaticTaskListView, where you declare the tasks as @State, or properly pass in state as you had in your old view.

struct StaticTaskListView: View {
    let title: String
    @State var tasks: [Task]
    let toggleAction: (Task) -> Void
    let deleteAction: (Task) -> Void
    let isStrikethrough: Bool
...
}