Search code examples
iosswiftuitableviewfirebasefirebaseui

Swift 3 - iOS 10 UITableView enabling "swipe-to-delete"


There are a lot of questions about how to enable swipe-to-delete for a UITableView, and they all say the same thing:

Override tableView(_:commit editingStyle:forRowAt indexPath:).

I have done this, among other things, and I still do not have the swipe-to-delete functionality. Things I've tried:

  1. Setting tableView.allowsMultipleSelectionDuringEditing to true and false, both in code and in IB.
  2. Overriding tableView(_:canEditRowAt indexPath:) and returning true.
  3. Overriding tableView(_:editingStyleForRowAt indexPath:) and returning .delete.
  4. And every combination of above.

I'm using FirebaseUI with a custom UITableViewCell to populate the table. Here's my table view controller:

import UIKit
import FirebaseDatabaseUI

class ScheduleViewController: UITableViewController {

    private let TAG = String(describing: ScheduleViewController.self)

    private var dataSource: FUITableViewDataSource!

    override func viewDidLoad() {
        super.viewDidLoad()

        dataSource = self.tableView.bind(to: DataManager.instance.habitsQuery(),
                                         populateCell: populateCell())

        self.tableView.dataSource = dataSource

        // Automatically resize table cells to fit its content.
        self.tableView.estimatedRowHeight = ScheduleTableViewCell.HEIGHT
        self.tableView.rowHeight = UITableViewAutomaticDimension

        // I have also
        self.tableView.allowsMultipleSelectionDuringEditing = false
    }

    func populateCell() -> (UITableView, IndexPath, FIRDataSnapshot) -> UITableViewCell {
        return { tableView, indexPath, snapshot in
            let cell =
                tableView.dequeueReusableCell(withIdentifier: ScheduleTableViewCell.IDENTIFIER,
                                              for: indexPath) as! ScheduleTableViewCell

            if let dict = snapshot.value as? Dictionary<String, Any?> {
                cell.set(habit: Habit(withKey: snapshot.key, from: dict))
            } else {
                Log.e(self.TAG, "Invalid data returned from Firebase.")
            }

            return cell
        }
    }

    // MARK: TableView Delegate

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    override func tableView(_ tableView: UITableView,
                            editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle
    {
        return .delete
    }

    override func tableView(_ tableView: UITableView,
                            commit editingStyle: UITableViewCellEditingStyle,
                            forRowAt indexPath: IndexPath)
    {

    }

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    }

}

Solution

  • Recent FirebaseUI updates have broken the original answer.

    UPDATED ANSWER:

    Simply subclass FUITableViewDataSource to implement custom UITableViewDataSource functionality, then bind the subclass to your UITableView.

    The FUITableViewDataSource subclass:

    import UIKit
    import FirebaseDatabaseUI
    
    class EditableTableDataSource: FUITableViewDataSource {
    
        /// Called to populate each cell in the UITableView.
        typealias PopulateCellBlock = (UITableView, IndexPath, FIRDataSnapshot) -> UITableViewCell
    
        /// Called to commit an edit to the UITableView.
        typealias CommitEditBlock = (UITableView, UITableViewCellEditingStyle, IndexPath) -> Void
    
        private let commitEditBlock: CommitEditBlock?
    
        /// A wrapper around FUITableViewDataSource.init(query:view tableView:populateCell:), with the
        /// addition of a CommitEditBlock.
        public init(query: FIRDatabaseQuery,
                    populateCell: @escaping PopulateCellBlock,
                    commitEdit: @escaping CommitEditBlock)
        {
            commitEditBlock = commitEdit
            super.init(collection: FUIArray.init(query: query), populateCell: populateCell)
        }
    
        override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
            return true
        }
    
        override func tableView(_ tableView: UITableView,
                                commit editingStyle: UITableViewCellEditingStyle,
                                forRowAt indexPath: IndexPath)
        {
            if (commitEditBlock != nil) {
                commitEditBlock!(tableView, editingStyle, indexPath)
            }
        }
    
    }
    
    extension UITableView {
    
        /// Creates a data source, binds it to the table view, and returns it. Note that this is the
        /// `EditableTableViewDataSource` equivalent of the 
        /// `FUITableViewDataSource.bind(to:populateCell:)` method.
        ///
        /// - parameters:
        ///   - to:             The Firebase query to bind to.
        ///   - populateCell:   A closure that's called to populate each cell.
        ///   - commitEdit:     A closure that's called when the user commits some kind of edit. Maps to
        ///                     `tableView(:commit:forRowAt:)`.
        func bind(to query: FIRDatabaseQuery,
                  populateCell: @escaping EditableTableDataSource.PopulateCellBlock,
                  commitEdit: @escaping EditableTableDataSource.CommitEditBlock)
            -> EditableTableDataSource
        {
            let dataSource = EditableTableDataSource(query: query,
                                                     populateCell: populateCell,
                                                     commitEdit: commitEdit)
            dataSource.bind(to: self)
            return dataSource
        }
    
    }
    

    And usage:

    import UIKit
    import FirebaseDatabaseUI
    
    class ScheduleViewController: UITableViewController {
    
        private let TAG = String(describing: ScheduleViewController.self)
    
        private var dataSource: FUITableViewDataSource!
        private var dataManager: DataManager!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            dataManager = AppManager.defaultInstance.dataManager()
    
            dataSource = tableView.bind(
                to: dataManager.scheduledHabitsQuery(),
                populateCell: populateCellBlock(),
                commitEdit: commitEditBlock())
        }
    
    
        // MARK: TableView Data Source
    
        func populateCellBlock() -> EditableTableDataSource.PopulateCellBlock {
            return { tableView, indexPath, snapshot in
                let cell = ScheduledHabitTableViewCell.from(tableView: tableView, at: indexPath)
                cell.set(habit: ScheduledHabit(fromSnapshot: snapshot))
                return cell
            }
        }
    
        func commitEditBlock() -> EditableTableDataSource.CommitEditBlock {
            return { tableView, editingStyle, indexPath in
                if (editingStyle != .delete) {
                    return
                }
    
                // Delete the data from Firebase.
                let snapshot = self.dataSource.snapshot(at: indexPath.row)
                self.dataManager.moveToTrash(ScheduledHabit(fromSnapshot: snapshot))
    
                // Deleting the table view row is done automatically by the FirebaseUI data source.
            }
        }
    
    
        // MARK: - Navigation
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        }
    
    }
    

    ORIGINAL ANSWER:

    The solution is to subclass FUITableViewDataSource and override the UITableViewDataSource methods you want. Everything worked perfectly after that.

    import UIKit
    import FirebaseDatabaseUI
    
    class FUIEditableTableViewDataSource: FUITableViewDataSource {
    
        /// Called to populate each cell in the UITableView.
        typealias PopulateCellBlock = (UITableView, IndexPath, FIRDataSnapshot) -> UITableViewCell
    
        /// Called to commit an edit to the UITableView.
        typealias CommitEditBlock = (UITableView, UITableViewCellEditingStyle, IndexPath) -> Void
    
        private let commitEditBlock: CommitEditBlock?
    
        /// A wrapper around FUITableViewDataSource.init(query:view tableView:populateCell:), with the
        /// addition of a CommitEditBlock.
        public init(query: FIRDatabaseQuery,
                    tableView: UITableView,
                    populateCell: @escaping PopulateCellBlock,
                    commitEdit: @escaping CommitEditBlock)
        {
            commitEditBlock = commitEdit
            super.init(query: query, view: tableView, populateCell: populateCell)
        }
    
        override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
            return true
        }
    
        override func tableView(_ tableView: UITableView,
                                commit editingStyle: UITableViewCellEditingStyle,
                                forRowAt indexPath: IndexPath)
        {
            if (commitEditBlock != nil) {
                commitEditBlock!(tableView, editingStyle, indexPath)
            }
        }
    
    }