How can I use Drag and Drop to reorder a UITableView? - ios

I know that drag and drop can be used to transfer data throughout and between apps. I'm not interested in that. All I want is to use drag and drop functionality to reorder a table view without transferring data. How do I do that?

If you are performing a drag on a single item locally, you can use tableView(_:moveRowAt:to:). In order to do this, you need to implement UITableViewDragDelegate.
Setup
Start by setting your delegates. Setting dragInteractionEnabled is required for iPhones.
func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.dragDelegate = self
tableView.dragInteractionEnabled = true
}
UITableViewDragDelegate
Notice that the array is returning a single item. If you return more than one item, then the UITableViewDropDelegate methods will be used instead of tableView(_:moveRowAt:to:). You must set a local object.
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let dragItem = UIDragItem(itemProvider: NSItemProvider())
dragItem.localObject = data[indexPath.row]
return [ dragItem ]
}
Moving
This is where the moving happens and is actually a part of UITableViewDelegate.
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Update the model
let mover = data.remove(at: sourceIndexPath.row)
data.insert(mover, at: destinationIndexPath.row)
}
You can also use tableView(_:canMoveRow:at:) and tableView(_:targetIndexPathForMoveFromRowAt: toProposedIndexPath:) if needed.
You can read more here...
Drag and Drop
Adopting Drag and Drop in a Table View

I found the answer for this really confusing, and found that implementing UITableViewDragDelegate is not what is required here. Basically:
If you want to just be able to reorder a table view using drag and drop then don't implement UITableViewDragDelegate and instead just implement the func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) method of UITableViewDelegate as such:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Update the model
let mover = data.remove(at: sourceIndexPath.row)
data.insert(mover, at: destinationIndexPath.row)
}
Otherwise if you want to be able to drag and drop items between apps or views etc then you will need to implement the UITableViewDragDelegate and the methods that come with that, e.g,
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]{
// Handle here
}
and
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
// Handle Drop Functionality
}
etc. Not going into too much detail on that, but you will find tutorials on dragging / dropping between different views and apps elsewhere. However, for a simple reordering of tableview cells using drag and drop, go with the first option and don't implement UITableViewDragDelegate.
Hope this is helpful, as I got a bit confused between the two approaches.

You can adopt Drag and Drop in a Table View
In order to do that, in addition to regular delegate = self and dataSource = self, you need to add
tableView.dragInteractionEnabled = true // Enable intra-app drags
tableView.dragDelegate = self
tableView.dropDelegate = self
That's it. The rest of it is to figure out how to implement the delegate methods above for both visual effects and data transfer.
When data is transferred between apps, the receiving app implements the methods of dropDelegate, the sending app implements the methods of dragDelegate. Now it's the same table view, it implements both.
You can stop reading now, instead, you can read the link at the top. Below is my understanding of the linked document. I'm half-baked in this topic, but I have made it work myself.
For dragDelegate
Implement func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] for providing data. In many/most cases, [UIDragItem] array only contains 1 element, cuz usually only one item is dragged. UIDragItem contains an NSItemProvider object which carries transferred data.
For dropDelegate
Implement func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool to tell the table view whether this drop action should be process.
Implement func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal to add a visual indication of dropping item. .move will add a "moving there" animation, .copy will add a plus sign to indicate copy action. If this func is not implemented, you get no visual cue, and I don't recommend it, but it's still optional.
Implement func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) for actual dropping the data. Regular moving rows is like moving a row from A to B. A is the original and B is the destination. Now in the case of drag and drop, one way you can do is that, you can get destination B from coordinator.destinationIndexPath. A's indexPath can be stored beforehand in the dragDelegate method UIDragItem's itemProvider, and now get it back thru coordinator.session.loadObject(ofClass:). Now you have A and B, you can either do tableView.move(at:to:) or delete one row then insert the row to the new indexPath.
Again, you can read Apple's document in the link at the top.

You dont need UITableViewDragDelegate for this.
First add this when ever you want to start editiyng/rearranging you table:
table.isEditing = true
Then add these delegate methods:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
allRides.swapAt(sourceIndexPath.row, destinationIndexPath.row)
}

After Swift 4 try this:
override func viewDidLoad() {
super.viewDidLoad()
self.deviceOrderingTableView.delegate = self
self.deviceOrderingTableView.dataSource = self
}
extension DevicesOrderingVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//configure Cells here
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
true
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Update the model
self.model.swapAt(sourceIndexPath.row, destinationIndexPath.row)
debugPrint(self.model)
self.deviceOrderingTableView.reloadData()
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return UITableViewCellEditingStyle.none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
}
Output image:

Related

Manually set list order in swift? (iOS)

I'm new to iOS development and now I'm trying to achieve this effect:
As you can see, when dragging, the cell's background is transparent and items above or below it will move away with a fluent animation.
Either UIKit or SwiftUI will do. Any advice will be appreciated. :)
This is achievable with the help of table delegates.Just implement below methods.
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .none
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let itemToReorder = yourList[sourceIndexPath.row]
yourList.remove(at: sourceIndexPath.row)
yourList.insert(itemToReorder, at: destinationIndexPath.row)
}
Don't forget to enable disable editing mode when needed or not.
Use this for enabling and disabling
tableView.setEditing(true, animated: true)

Is it possible to customise UITableView's reorder icon to locate at left side?

It is possible to customise UITableView edit mode to certain extend.
By conform the following protocol funcions
// MARK: Customization
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return UITableViewCell.EditingStyle.none
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
return proposedDestinationIndexPath
}
We are able to have something as
Currently, our requirement is that, the reoder icon (icon with 3 horizontal lines) should be at left side. It should look as
I was wondering, during UITableView edit mode, is it ever possible to customise the reorder icon on left side?
In the cell's willDisplayCell you can iterate through the cell's subviews until you find the reorder control. Create a new subview the same size as the control and place it where you want, then add the reorder control as a subview of the subview you created and center it on it.
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.subviews.forEach { (subview) in
if String(describing: type(of: subview.self)) == "UITableViewCellReorderControl" {
let control = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: subview.frame.size))
control.addSubview(subview)
subview.center = control.center
cell.contentView.addSubview(control)
}
}
}

iOS Allow UITableViewCell swipe deletion and rearrangeable cells simultaneously

In order to enable rearrangeable (moveable) cells in my UITableView, I must set table.isEditing to true and implement the following delegate methods:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
I also implement the following delegate methods to hide the default table-editing UI:
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
I would also like to swipe-delete the cells in this table. I've tried using both the tableView(_:editActionsForRowAt:) approach and the new iOS 11 tableView(_:trailingSwipeActionsConfigurationForRowAt:) but neither work while table.isEditing = true. The only other relevant table delegate method I use is:
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool
which returns true for all cells.
Is there a way to enable both of these UITableView features simultaneously? It looks possible as this is exactly how the table queue in the Apple Music app works:

Drag and drop works for UITableView without UITableViewDropDelegate

According to the Apple documentation to support drag and drop for UITableView I have to implement UITableViewDragDelegate and UITableViewDropDelegate. I implemented only UITableViewDragDelegate with fake implementation for tableView(_:itemsForBeginning:at:) (it return empty list). But this solution works.
Why does it work?
import UIKit
class TableViewController: UITableViewController, UITableViewDragDelegate {
var data = [1, 2, 3, 4, 5]
// MARK: - Table view drag delegate
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
// fake implementation with empty list
return []
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Cell \(data[indexPath.row])"
return cell
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let tmp = data[sourceIndexPath.row]
data.remove(at: sourceIndexPath.row)
data.insert(tmp, at: destinationIndexPath.row)
}
// MARK: - View controller
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dragInteractionEnabled = true
self.tableView.dragDelegate = self
}
}
It doesn't work. What you are seeing is not drag and drop.
You are seeing row rearrangment, using the "reorder control". This mechanism is very old; it existed for many years before drag and drop was created. That is the effect of your implementation of
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
It is a UITableViewDataSource method; it has nothing to do with drag and drop. See:
https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614867-tableview
If you delete that method, the old row rearrangement mechanism will cease to operate, and now you'll be using drag and drop and your implementation of
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
will be meaningful.

swift drag drop tableviewcontroller

Before I've just used tableView.setEditing(true, animated: true) in order to make rows in tableView move. I used the code:
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
array.insert(array.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
I've found that there is an interesting protocols: UITableViewDragDelegate and UITableViewDropDelegate. I have to implement 2 funcs and add tableView.dragDelegate = self; tableView.dropDelegate = self; tableView.dragInteractionEnabled = true
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
return []
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}
What is surprising to me is that this code is enough to make rows move. And I have a simple question - is it that simple? May be I miss something, may be I have to add something?

Resources