0

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

wayneh
  • 4,393
  • 9
  • 35
  • 70
  • Possible duplicate of [Swipe-able Table View Cell in iOS 9](http://stackoverflow.com/questions/32004557/swipe-able-table-view-cell-in-ios-9) –  Mar 05 '17 at 19:52
  • That is not what OP is asking about – Michael Fourre Mar 05 '17 at 19:54
  • @MichaelFourre is correct. However, I have just found two posts that show what I'm looking for, and one which is almost exactly it. GENERAL: https://www.raywenderlich.com/77974/making-a-gesture-driven-to-do-list-app-like-clear-in-swift-part-1 EXACT: https://gabrielghe.github.io/swift/2016/03/20/swipable-uitableviewcell – wayneh Mar 05 '17 at 20:02
  • You could write up your solution and then when you're done post an answer to yourself with some snippets of the implementation in case someone else wants to see it – Michael Fourre Mar 05 '17 at 20:04
  • Will do once I figure it out! Having some minor issues since the examples are not in Swift 3 – wayneh Mar 05 '17 at 20:19

1 Answers1

7

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
        var touch = CGPoint()
    
    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")
                touch = panGestureRecognizer.location(in: superview)
                return true
            }
            //not left or right - must be up or down
            return false
        }else if gestureRecognizer is UITapGestureRecognizer {
            touch = gestureRecognizer.location(in: superview)
            return true
        }
        return false
    }
    
    func handleTap(_ recognizer: UITapGestureRecognizer){
        // 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{
                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.

wayneh
  • 4,393
  • 9
  • 35
  • 70