wayneh wayneh - 9 months ago 104
iOS Question

How to add a Swipe Left gesture to a TableView and return the cell location when swiped in Swift 3

I have a TableView which is already coded to perform an action when any cell it selected using the

didSelectRowAt
method.

Now, I'd like to add a Swipe Left gesture to the Table (or cell) so that I can perform a secondary action when a cell is swiped rather than when tapped.

1) I would like the cell to move left while swiping but I do NOT want to add a button in the space where the cell has moved from.

2) Instead, I'd like to be able to 'drag' the cell left until a certain point (say halfway) and at that point execute the secondary action with the indexPath (so I know which cell was dragged).

3) If the user stops dragging or lets go of the cell, I'd like it to return to it's starting position and have no actions occur.

I've seen a lot of samples that do various pieces of this but most are in Obj-C or insert buttons in the same row as the cell.

Also, is it better to add the Gesture to each cell?
It seems smarter to add it to the table...

EDIT: See below for my complete answer with code

Answer Source

I've done some research and created a bare-bones example of how to this - create a table cell that can be swiped AND tapped. I'm using it in a music player - tap a cell and play the song, swipe the same cell and segue to a different view.

I've built my solution based on these two existing samples: https://www.raywenderlich.com/77974/making-a-gesture-driven-to-do-list-app-like-clear-in-swift-part-1

https://gabrielghe.github.io/swift/2016/03/20/swipable-uitableviewcell

I don't have a repository to share so all the code is here.

I'm using Xcode 8.2.1 and Swift 3

STEP 1:

  • Create a New, Single-View Swift project, and open the Storyboard.
  • Drag a TableView onto the existing ViewController and drag it to fit the view.

STEP 2:

  • Add new Swift File to the Project and name it "TableViewCellSliding.swift".
  • Copy/paste the code below into the new file.

    //
    //  TableViewCellSliding.swift
    //
    
    import UIKit
    
    protocol SlidingCellDelegate {
        // tell the TableView that a swipe happened
        func hasPerformedSwipe(touch: CGPoint)
        func hasPerformedTap(touch: CGPoint)
    }
    
    class SlidingTableViewCell: UITableViewCell {
        var delegate: SlidingCellDelegate?
        var originalCenter = CGPoint()
        var isSwipeSuccessful = false
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    
        // add a PAN gesture
        let pRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SlidingTableViewCell.handlePan(_:)))
        pRecognizer.delegate = self
        addGestureRecognizer(pRecognizer)
    
        // add a TAP gesture
        // note that adding the PAN gesture to a cell disables the built-in tap responder (didSelectRowAtIndexPath)
        // so we can add in our own here if we want both swipe and tap actions
        let tRecognizer = UITapGestureRecognizer(target: self, action: #selector(SlidingTableViewCell.handleTap(_:)))
        tRecognizer.delegate = self
        addGestureRecognizer(tRecognizer)
    }
    
    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
            let translation = panGestureRecognizer.translation(in: superview!)
            //look for right-swipe
            if (fabs(translation.x) > fabs(translation.y)) && (translation.x > 0){
    
            // look for left-swipe
            //if (fabs(translation.x) > fabs(translation.y)) && (translation.x < 0){
                //print("gesture 1")
                return true
            }
            //not left or right - must be up or down
            return false
        }else if gestureRecognizer is UITapGestureRecognizer {
            return true
        }
        return false
    }
    
    func handleTap(_ recognizer: UITapGestureRecognizer){
        let touch = recognizer.location(in: superview)
        // call function to get indexPath since didSelectRowAtIndexPath will be disabled
        delegate?.hasPerformedTap(touch: touch)
    }
    
    func handlePan(_ recognizer: UIPanGestureRecognizer) {
        if recognizer.state == .began {
            originalCenter = center
        }
    
        if recognizer.state == .changed {
            checkIfSwiped(recongizer: recognizer)
        }
    
        if recognizer.state == .ended {
            let originalFrame = CGRect(x: 0, y: frame.origin.y, width: bounds.size.width, height: bounds.size.height)
            if isSwipeSuccessful{
                let touch = recognizer.location(in: superview)
                delegate?.hasPerformedSwipe(touch: touch)
    
                //after 'short' swipe animate back to origin quickly
                moveViewBackIntoPlaceSlowly(originalFrame: originalFrame)
            } else {
                //after successful swipe animate back to origin slowly
                moveViewBackIntoPlace(originalFrame: originalFrame)
            }
        }
    }
    
    func checkIfSwiped(recongizer: UIPanGestureRecognizer) {
        let translation = recongizer.translation(in: self)
        center = CGPoint(x: originalCenter.x + translation.x, y: originalCenter.y)
    
        //this allows only swipe-right
        isSwipeSuccessful = frame.origin.x > frame.size.width / 2.0  //pan is 1/2 width of the cell
    
        //this allows only swipe-left
        //isSwipeSuccessful = frame.origin.x < -frame.size.width / 3.0  //pan is 1/3 width of the cell
    }
    
    func moveViewBackIntoPlace(originalFrame: CGRect) {
        UIView.animate(withDuration: 0.2, animations: {self.frame = originalFrame})
    }
    func moveViewBackIntoPlaceSlowly(originalFrame: CGRect) {
        UIView.animate(withDuration: 1.5, animations: {self.frame = originalFrame})
    }
    
    }
    

STEP 3:

  • Copy/paste the code below into the existing file "ViewController.swift"

    //
    //  ViewController.swift
    //
    
    import UIKit
    
    class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SlidingCellDelegate {
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(SlidingTableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.rowHeight = 50;
    }
    
    func hasPerformedSwipe(touch: CGPoint) {
        if let indexPath = tableView.indexPathForRow(at: touch) {
            // Access the image or the cell at this index path
            print("got a swipe row:\(indexPath.row)")
        }
    }
    
    func hasPerformedTap(touch: CGPoint){
        if let indexPath = tableView.indexPathForRow(at: touch) {
        // Access the image or the cell at this index path
            print("got a tap row:\(indexPath.row)")
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int)-> Int {
        return 100
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell=tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath) as! SlidingTableViewCell
        // Configure cell
        cell.selectionStyle = .none
        cell.textLabel?.text = "hello \(indexPath.row)"
        cell.delegate = self
        return cell
    }
    func tableView(sender: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        // do stuff with indexPath.row and indexPath.section
        //never make it here because we added a tap gesture but this will 
        print("selected cell")
    }
    }
    

STEP 4:

Connect it all in the Storyboard.

  • Open the Storyboard view and select the TableView. Go to the Connections Inspector (upper-right corner arrow in a circle) and drag from New Referencing Outlet to the TableView and select "tableView" from the popup menu.

  • With the TableView still selected, drag from Outlets > dataSource to the TableView in the Storyboard. Repeat starting with Outlets > delegate.

STEP 5:

  • Run it!

I'm not going into details about any of the code as the two links at the top do that very well. This is just about having complete, simple, clean code that you can build on. Enjoy.