UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: UITableView swipe actions iOS11+/Swift 5

Forums > iOS

I want to have swipe actions on my tableview. This has been changed by Apple for iOS11+/Swift5. I've implemented the

UISwipeActionsConfiguration

But I cannot swipe left to right or visa versa. Should I set something on the tableview as well? @Nemecek-Filip, I saw your blog post about this, do you have an idea?

//MARK: - Ext. TableViewDelegate
extension AnnotationsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let annotationType = AnnotationType.allCases[section]
        let cell = tableView.dequeueReusableHeaderFooterView(withIdentifier: AnnotationHeaderCell.reuseIdentifier) as! AnnotationHeaderCell

        cell.configure(with: annotationType)

        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let viewController = UIViewController()
        viewController.view.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
        viewController.modalPresentationStyle = .formSheet
        viewController.view.backgroundColor = .blue
//        navigationController?.pushViewController(viewController, animated: true)
        navigationController?.show(viewController, sender: self)
    }

    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        return UISwipeActionsConfiguration(actions: [
            makeCompleteContextualAction(forRowAt: indexPath)
        ])
    }

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        return UISwipeActionsConfiguration(actions: [
            makeDeleteContextualAction(forRowAt: indexPath)
        ])
    }

    //MARK: - Contextual Actions
    private func makeDeleteContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction {
        return UIContextualAction(style: .destructive, title: "Delete") { (action, swipeButtonView, completion) in
            print("DELETE HERE")

            completion(true)
        }
    }

    private func makeCompleteContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction {
        return UIContextualAction(style: .normal, title: "Complete") { (action, swipeButtonView, completion) in
            action.image = ProjectImages.Annotation.checkmark
            action.image?.withTintColor(.systemGreen)
            action.backgroundColor = .systemOrange
            print("COMPLETE HERE")
            completion(true)
        }
    }
}

3      

Hi! This should be enough to have the actions. But of course table view has a habit of trolling developers I think :D If you try comment out one of the method and leave just trailing for example, any change?

Also just to safe, did you set your VC as table view delegate?

3      

@Nemecek-Filip I've set the delegate and it works, because didSelectRowAt is triggered ....

    private func configureTableView() {
        tableView = UITableView(frame: view.bounds, style: .grouped)
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.backgroundColor = .clear

        tableView.register(AnnotationCell.self, forCellReuseIdentifier: AnnotationCell.reuseIdentifier)
        tableView.register(AnnotationHeaderCell.self, forHeaderFooterViewReuseIdentifier: AnnotationHeaderCell.reuseIdentifier)

        //Dynamic sizing cells
        tableView.sectionHeaderHeight = 40
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 70

        tableView.isUserInteractionEnabled = true

    }

I've tried several combinations, but can't get it working 🥵Had too many UITableView issues lately and getting resolve is hard with all the different solutions for different versions of swift and iOS ...

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Do you have some sort of custom gesture recognizers on this screen?

3      

No, I have not ...

This the complete viewcontroller (now with your style (from the blog) contextual actions:

class AnnotationsViewController: UIViewController {
    //MARK: - Types
    typealias Annotation = AnnotationsController.Annotation
    typealias AnnotationType = AnnotationsController.AnnotationType

    //MARK: - Properties
    let annotationsController = AnnotationsController(with: ProjectsController.activeProject!)

    var tableView: UITableView!
    var dataSource: UITableViewDiffableDataSource<AnnotationType, Annotation>!

    //MARK: - Viewcontroller Methods
    override func viewDidLoad() {
        super.viewDidLoad()

        configureViewController()
        configureTableView()
        createDataSource()
        updateData(on: annotationsController.filteredAnnotations())
//        configureSearchController()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.tintColor = .systemOrange
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        navigationController?.navigationBar.tintColor = .systemPurple
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateFlipButton()
    }

    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)

        coordinator.animate(alongsideTransition: nil) { _ in
            self.updateFlipButton()
        }
    }

    //MARK: - SplitOrFlip functionality
    private func updateFlipButton() {
        if let splitOrFlipViewController = splitOrFlipViewController {
            switch splitOrFlipViewController.displayMode {
                case .sideBySide:
                    navigationItem.rightBarButtonItem = nil
                    break
                case .one:
                    navigationItem.rightBarButtonItem = UIBarButtonItem(image: ProjectImages.BarButton.flip, style: .plain, target: self, action: #selector(flipAction))
                    break
            }
        }
    }

    @objc func flipAction() {
        splitOrFlipViewController?.flipAction()
    }

    //MARK: - DataSource
    func createDataSource() {
        dataSource = UITableViewDiffableDataSource<AnnotationType, Annotation>(tableView: tableView) { (tableView, indexPath, annotation) in
            return self.configure(AnnotationCell.self, with: annotation, for: indexPath)
        }
    }

    func updateData(on annotations: [Annotation]) {
        var snapshot = NSDiffableDataSourceSnapshot<AnnotationType, Annotation>()
        //Append available sections
        AnnotationType.allCases.forEach { snapshot.appendSections([$0]) }

        //Append annotations to their corresponding sections
        annotations.forEach { (annotation) in
            snapshot.appendItems([annotation], toSection: annotation.type as AnnotationType)
        }

        //Force the update on the main thread to silence a warning about tableview not being in the hierarchy!
        DispatchQueue.main.async {
            self.dataSource.apply(snapshot, animatingDifferences: true)
        }
    }

    ///Configure any type of cell that conforms to selfConfiguringAnnotationCell!
    func configure<T: SelfConfiguringAnnotationCell>(_ cellType: T.Type, with annotation: Annotation, for indexPath: IndexPath) -> T {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
            fatalError("Unable to dequeue \(cellType)")
        }

        cell.configure(with: annotation)
        return cell
    }

    //MARK: - Actions    
    @objc private func closeProject() {
        //TODO: - Save Projects & Annotations

        //Reassign rootViewController
        UIApplication.resetRootViewControllerTo(ProjectsViewController())
    }

    //MARK: - UI & layout
    private func configureViewController() {
        title = annotationsController.project.title
        view.backgroundColor = .systemPurple
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.tintColor = .systemOrange
        navigationItem.leftBarButtonItem = UIBarButtonItem(image: ProjectImages.BarButton.close, style: .plain, target: self, action: #selector(closeProject))
    }

    private func configureTableView() {
        tableView = UITableView(frame: view.bounds, style: .grouped)
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.backgroundColor = .clear

        tableView.register(AnnotationCell.self, forCellReuseIdentifier: AnnotationCell.reuseIdentifier)
        tableView.register(AnnotationHeaderCell.self, forHeaderFooterViewReuseIdentifier: AnnotationHeaderCell.reuseIdentifier)

        //Dynamic sizing cells
        tableView.sectionHeaderHeight = 40
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 70
    }
}

//MARK: - Ext. TableViewDelegate
extension AnnotationsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let annotationType = AnnotationType.allCases[section]
        let cell = tableView.dequeueReusableHeaderFooterView(withIdentifier: AnnotationHeaderCell.reuseIdentifier) as! AnnotationHeaderCell

        cell.configure(with: annotationType)

        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//        let viewController = UIViewController()
//        viewController.view.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
//        viewController.modalPresentationStyle = .formSheet
//        viewController.view.backgroundColor = .blue
////        navigationController?.pushViewController(viewController, animated: true)
//        navigationController?.show(viewController, sender: self)
    }

    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let completeAction = UIContextualAction(style: .normal, title: nil) { (_, _, completionHandler) in
            // delete the item here
            completionHandler(true)
        }
        completeAction.image = ProjectImages.Annotation.checkmark.withTintColor(.systemGreen)
        completeAction.backgroundColor = .systemOrange
        let configuration = UISwipeActionsConfiguration(actions: [completeAction])
        return configuration
    }

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let deleteAction = UIContextualAction(style: .destructive, title: nil) { (_, _, completionHandler) in
            // delete the item here
            completionHandler(true)
        }
        deleteAction.image = UIImage(systemName: "trash")
        deleteAction.backgroundColor = .systemRed
        let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
        return configuration
    }

}

3      

I see you are using diffable data source but I dont think that should change how the swipe actions behave with the delegate :/

Oh well maybe it does - https://stackoverflow.com/questions/57898044/unable-to-swipe-to-delete-with-tableview-using-diffable-data-source-in-ios-13

3      

Oh no, you got to be kidding! Hate these inconsistencies. Should I really implement swipe actions on my own? (I did that on my collectionview already)

🥵

3      

What about the solution of subclassing diffable datasource and allowing them as per SO answer?

4      

I would definitely stick with system ones because they are highly polished and solve a lot for you

4      

@Nemecek-Filip.... Yes I guess I have to subclass the datasource to ve able to implement canEditRowAt ...

You should subclass UITableViewDiffableDataSource and return true for the rows you want to enable this for in:

override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool

Another puzzle to solve, gonna try that, no idea how yet though ....

3      

I think you can get away with just creating something like SwipeEnabledDiffableDataSource, inherit from UITableViewDiffableDataSource and "just" override the method canEditRowAt to return true.

4      

Yes, you are right:

class AnnotationDiffableDataSource: UITableViewDiffableDataSource<AnnotationsController.AnnotationType, AnnotationsController.Annotation> {

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

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        //
    }
}

And in my viewController, only change the DataSource to be of your subclass and change the call to:

    var dataSource: AnnotationDiffableDataSource!

    //MARK: - DataSource
    func createDataSource() {
        dataSource = AnnotationDiffableDataSource(tableView: tableView) { (tableView, indexPath, annotation) in
            return self.configure(AnnotationCell.self, with: annotation, for: indexPath)
        }
    }

This works. Would be neater to move more over to the Subclass, but I am unsure how to do that (No access to the tableview there) Any resources on a fully subclassed datasource appreciated!

3      

3      

Heyy :) Here is another way that has worked for me, see editingStyle func:


class DataSource: UITableViewDiffableDataSource<TaskType, Task> {

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return TaskType.allCases[section].rawValue // "One Large" / "Three medium" etc..
    }

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

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete{
            var snapshot = self.snapshot()
            if let item = itemIdentifier(for: indexPath) {
                snapshot.deleteItems([item])
                apply(snapshot, animatingDifferences: true)
            }
        }
    }

}

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.