iosswiftuitableviewswipe-gesture

How to swipe-to-delete in a TableView with custom Table View cells?


Currently, I have a screen that shows a TableView with custom cells that each have one text field and one button. I am storing the text field's data inputs into taskName dict and the button's title selection (from a pop-up screen) into taskTime dict.

However, my issue is when I swipe-to-delete a row in the TableView, it deletes the data stored in the row, but doesn't delete the row itself (as seen in the animation below).

Here is my code:

Screen with TableView + custom cells

class TaskListViewController: UIViewController {
    
    @IBOutlet weak var taskList: SelfSizedTableView!
    
    var taskCount: Int = 1
    var taskName = [Int:String]()
    var taskTime = [Int:String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set initial taskTime value
        taskTime[0] = "Set time"

        taskList.delegate = self
        taskList.dataSource = self
    }
}

extension TaskListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int {
        return taskCount
    }
    
    // Return custom cell + data to insert in table view
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as! TaskCell
        
        cell.delegate = self
        
        // Configure nameField and timeButton in taskCell
        cell.nameField.text = taskName[indexPath.row]
        cell.timeButton.setTitle(taskTime[indexPath.row], for: .normal)
        
        return cell
    }
    
    func tableView(_ tableView: UITableView,
                   commit editingStyle: UITableViewCell.EditingStyle,
                   forRowAt indexPath: IndexPath) {

        // Update # of rows in taskList
        taskCount -= 1

        // Row that is deleted
        let deleteRowIndex = indexPath.row
        print(deleteRowIndex)

        // Remove taskName + taskTime from dictionary
        taskName[deleteRowIndex] = nil
        taskTime[deleteRowIndex] = nil

        // Delete row from table view
        let indexPaths = [indexPath]
        taskList.deleteRows(at: indexPaths, with: .fade)
        
        // Reload table view with new data
        taskList.reloadData()
    }
}

enter image description here


Solution

  • First of all don't use two dictionaries as data source use one array of a custom struct

    struct Task {
        var name, time : String
    }
    

    var tasks = [Task]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set initial taskTime value
        tasks.append(Task(name:"", time: "Set time"))
    
        taskList.delegate = self
        taskList.dataSource = self
    }
    

    And don't hard-code the number of cells, count the array, taskCount is not needed.

    extension TaskListViewController: UITableViewDataSource {
        
        func tableView(_ tableView: UITableView,
                       numberOfRowsInSection section: Int) -> Int {
            return tasks.count
        }
        
        // Return custom cell + data to insert in table view
        func tableView(_ tableView: UITableView,
                       cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as! TaskCell
            
            cell.delegate = self
            
            // Configure nameField and timeButton in taskCell
            let task = tasks(indexPath.row)
            cell.nameField.text = task.name
            cell.timeButton.setTitle(task.time, for: .normal)
            
            return cell
        }
    

    And it's highly recommended to use tableView(_:trailingSwipeActionsConfigurationForRowAt: rather than tableView(_:commit:forRowAt

    In this method remove the item at the given index path and call deleteRows(at:with:). Calling reloadData() right after deleteRows(at:with:) is redundant. The latter methods updates the UI

        override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
            
            let delete = UIContextualAction(style: .destructive, title: "Delete") { [unowned self] action, view, completionHandler in
                self.tasks.remove(at: indexPath.row) 
                tableView.deleteRows(at: [indexPath], with: .fade)
                completionHandler(true)
            }
            return UISwipeActionsConfiguration(actions: [delete])
        }
    }
    

    Of course you have to refactor the other occurrences of the two data source dictionaries to match the data source array.