27

I want to be able to reorder tableview cells using a longPress gesture (not with the standard reorder controls). After the longPress is recognized I want the tableView to essentially enter 'edit mode' and then reorder as if I was using the reorder controls supplied by Apple.

Is there a way to do this without needing to rely on 3rd party solutions?

Thanks in advance.

EDIT: I ended up using the solution that was in the accepted answer and relied on a 3rd party solution.

Michael Campsall
  • 4,325
  • 11
  • 37
  • 52
  • hey im trying to do the same. What did you end up using? – snapfish Mar 02 '14 at 02:52
  • the Swift 3 code above works fine in Swift 4. Nice code, thanks to the author ! I made changes to enable a multi-section table backed by core data to work. As this code takes the place of *'moveRowAt fromIndexPath: IndexPath, to toIndexPath: IndexPath'* you need to replicate code from there into the long press recogniser function. By implementing the move row & update data code in the 'sender.state == .changed' you are updating every time. As I did not want all those unnecessary core data updates I moved the code into 'sender.state == .ended'. To enable this to work I had to store the initial – SundialSoft Oct 12 '18 at 11:44

6 Answers6

60

They added a way in iOS 11.

First, enable drag interaction and set the drag and drop delegates.

Then implement moveRowAt as if you are moving the cell normally with the reorder control.

Then implement the drag / drop delegates as shown below.

tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { }

extension TableView: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        return [UIDragItem(itemProvider: NSItemProvider())]
    }
} 

extension TableView: UITableViewDropDelegate {
    func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {

        if session.localDragSession != nil { // Drag originated from the same app.
            return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }

        return UITableViewDropProposal(operation: .cancel, intent: .unspecified)
    }

    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    }
}
Grant Kamin
  • 1,230
  • 11
  • 11
  • 1
    it works great , just remember to add the new index for you array in func moveRowAt let item = myArray[sourceIndexPath.row] myArray.remove(at: sourceIndexPath.row) myArray.insert(item, at: destinationIndexPath.row) – Yosra Nagati Sep 30 '19 at 04:43
  • 1
    This works even without setting the tableview.dropDelegate and declaring dropSessionDidUpdate. – Matt Rundle Oct 29 '19 at 18:40
  • 4
    This is the correct, quickest and easiest solution. Worked right the first time. Thank you for showing this. No need for third party libraries or stupidly long code. – zeeshan Dec 30 '19 at 16:00
  • 1
    It works great, but I'm getting a strange error: 'PBItemCollectionServicer connection disconnected' – jcr May 13 '20 at 16:15
  • @jcr - related - https://stackoverflow.com/a/46161396/5977215 – SymbolixAU Oct 23 '20 at 03:10
25

Swift 3 and no third party solutions

First, add these two variables to your class:

var dragInitialIndexPath: IndexPath?
var dragCellSnapshot: UIView?

Then add UILongPressGestureRecognizer to your tableView:

let longPress = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressGesture(sender:)))
longPress.minimumPressDuration = 0.2 // optional
tableView.addGestureRecognizer(longPress)

Handle UILongPressGestureRecognizer:

// MARK: cell reorder / long press

func onLongPressGesture(sender: UILongPressGestureRecognizer) {
  let locationInView = sender.location(in: tableView)
  let indexPath = tableView.indexPathForRow(at: locationInView)

  if sender.state == .began {
    if indexPath != nil {
      dragInitialIndexPath = indexPath
      let cell = tableView.cellForRow(at: indexPath!)
      dragCellSnapshot = snapshotOfCell(inputView: cell!)
      var center = cell?.center
      dragCellSnapshot?.center = center!
      dragCellSnapshot?.alpha = 0.0
      tableView.addSubview(dragCellSnapshot!)

      UIView.animate(withDuration: 0.25, animations: { () -> Void in
        center?.y = locationInView.y
        self.dragCellSnapshot?.center = center!
        self.dragCellSnapshot?.transform = (self.dragCellSnapshot?.transform.scaledBy(x: 1.05, y: 1.05))!
        self.dragCellSnapshot?.alpha = 0.99
        cell?.alpha = 0.0
      }, completion: { (finished) -> Void in
        if finished {
          cell?.isHidden = true
        }
      })
    }
  } else if sender.state == .changed && dragInitialIndexPath != nil {
    var center = dragCellSnapshot?.center
    center?.y = locationInView.y
    dragCellSnapshot?.center = center!

    // to lock dragging to same section add: "&& indexPath?.section == dragInitialIndexPath?.section" to the if below
    if indexPath != nil && indexPath != dragInitialIndexPath {
      // update your data model
      let dataToMove = data[dragInitialIndexPath!.row]
      data.remove(at: dragInitialIndexPath!.row)
      data.insert(dataToMove, at: indexPath!.row)

      tableView.moveRow(at: dragInitialIndexPath!, to: indexPath!)
      dragInitialIndexPath = indexPath
    }
  } else if sender.state == .ended && dragInitialIndexPath != nil {
    let cell = tableView.cellForRow(at: dragInitialIndexPath!)
    cell?.isHidden = false
    cell?.alpha = 0.0
    UIView.animate(withDuration: 0.25, animations: { () -> Void in
      self.dragCellSnapshot?.center = (cell?.center)!
      self.dragCellSnapshot?.transform = CGAffineTransform.identity
      self.dragCellSnapshot?.alpha = 0.0
      cell?.alpha = 1.0
    }, completion: { (finished) -> Void in
      if finished {
        self.dragInitialIndexPath = nil
        self.dragCellSnapshot?.removeFromSuperview()
        self.dragCellSnapshot = nil
      }
    })
  }
}

func snapshotOfCell(inputView: UIView) -> UIView {
  UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
  inputView.layer.render(in: UIGraphicsGetCurrentContext()!)
  let image = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()

  let cellSnapshot = UIImageView(image: image)
  cellSnapshot.layer.masksToBounds = false
  cellSnapshot.layer.cornerRadius = 0.0
  cellSnapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
  cellSnapshot.layer.shadowRadius = 5.0
  cellSnapshot.layer.shadowOpacity = 0.4
  return cellSnapshot
}
budiDino
  • 13,044
  • 8
  • 95
  • 91
6

You can't do it with the iOS SDK tools unless you want to throw together your own UITableView + Controller from scratch which requires a decent amount of work. You mentioned not relying on 3rd party solutions but my custom UITableView class can handle this nicely. Feel free to check it out:

https://github.com/bvogelzang/BVReorderTableView

bvogelzang
  • 1,405
  • 14
  • 17
2

So essentially you want the "Clear"-like row reordering right? (around 0:15)

This SO post might help.

Unfortunately I don't think you can do it with the present iOS SDK tools short of hacking together a UITableView + Controller from scratch (you'd need to create each row itself and have a UITouch respond relevant to the CGRect of your row-to-move).

It'd be pretty complicated since you need to get the animation of the rows "getting out of the way" as you move the row-to-be-reordered around.

The cocoas tool looks promising though, at least go take a look at the source.

Community
  • 1
  • 1
samuelsaumanchan
  • 617
  • 1
  • 5
  • 10
  • Yes that is exactly the type of reordering I want, thanks. I was hoping to avoid 3rd party solutions or "hacking together" things from scratch, but maybe it is unavoidable in this case. – Michael Campsall Sep 08 '12 at 03:04
1

There's a great Swift library out there now called SwiftReorder that is MIT licensed, so you can use it as a first party solution. The basis of this library is that it uses a UITableView extension to inject a controller object into any table view that conforms to the TableViewReorderDelegate:

extension UITableView {

    private struct AssociatedKeys {
        static var reorderController: UInt8 = 0
    }

    /// An object that manages drag-and-drop reordering of table view cells.
    public var reorder: ReorderController {
        if let controller = objc_getAssociatedObject(self, &AssociatedKeys.reorderController) as? ReorderController {
            return controller
        } else {
            let controller = ReorderController(tableView: self)
            objc_setAssociatedObject(self, &AssociatedKeys.reorderController, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return controller
        }
    }

}

And then the delegate looks somewhat like this:

public protocol TableViewReorderDelegate: class {

    // A series of delegate methods like this are defined:
    func tableView(_ tableView: UITableView, reorderRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)

}

And the controller looks like this:

public class ReorderController: NSObject {

    /// The delegate of the reorder controller.
    public weak var delegate: TableViewReorderDelegate?

    // ... Other code here, can be found in the open source project

}

The key to the implementation is that there is a "spacer cell" that is inserted into the table view as the snapshot cell is presented at the touch point, so you need to handle the spacer cell in your cellForRow:atIndexPath: call:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if let spacer = tableView.reorder.spacerCell(for: indexPath) {
        return spacer
    }
    // otherwise build and return your regular cells
}
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • It does quite clearly say in the question "without needing to rely on 3rd party solutions" – Sean Lintern Feb 26 '19 at 15:49
  • I found this question in a google search, and I went to the trouble of explaining how the project is put together without plagiarizing the source code. So, 1) it’s still a relevant answer to those looking to solve this problem, and 2) considering the source is MIT licensed, one could use the code directly in their project and make it first party. – brandonscript Feb 26 '19 at 16:09
  • This is the best solution I have found. I have tried the iOS11 Drag delegate and to say that it was glitchy and ugly is an understatement. SwiftReorder works better by a long shot then Apple's own reorder control. This is an excellent library. – Vlad Oct 24 '19 at 07:52
-4

Sure there's a way. Call the method, setEditing:animated:, in your gesture recognizer code, that will put the table view into edit mode. Look up "Managing the Reordering of Rows" in the apple docs to get more information on moving rows.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • 1
    Yes that will work, however I want it to be one action and not two. I want to skip the need for the reorder controls to appear and the user needing to use them to reorder the cells. It should be like this: LongPress initiates edit mode AND allows the user to drag the cell WITHOUT needing to lift their finger and press a reorder control. – Michael Campsall Sep 04 '12 at 14:17
  • 1
    jemicha, were you able to find a solution for this? I am looking to accomplish the same task. – kyleplattner Feb 19 '13 at 19:26
  • I'm looking for the same (enter edit mode and start reordering in one long press), any clue on how you did this? Please – Nicolas Manzini Dec 16 '13 at 11:15