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)
}
}
}
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:
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:
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?
I have a UITableView which I need to both reorder and allow for cells to be deleted by swiping left on a cell to reveal the Delete action. Is it possible to have both at the same time?
For the table I have:
tableView.allowsSelectionDuringEditing = true
tableView.isEditing = true
For the datasource:
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
// I do nothing here
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// This is where I rearrange my data source array
}
And for the delegate:
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "Remove") { action, index in
// delete row
}
delete.backgroundColor = UIColor.appRed
return [delete]
}
So what I want to have is:
Reorder control allowing me to move a cell
Swipe left to reveal cell actions
And I need these to be visible and work at the same time. Can this be done?
Swipe to delete is not possible when your UITableView is in editing mode.
If you want to hide the delete button on the left when in editing mode you have to implement the following methods:
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return tableView.isEditing ? .none : .delete
}
I think this is the closest you can get.