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.
Related
I have a table view with two sections and want to allow rows to be re-ordered for just one of the sections. I've found a lot of information about this, but can't restrict it to the single section. Here is my entire code - it's very simple, with one table view. I've put this together from various sources - thanks to those who have contributed to this topic.
To reproduce this in Storyboard, add a table view to the view controller, add some constraints, set number of Prototype Cells to 1, and set its Identifier to 'cell'.
The table view has two sections - 'Fruit' and 'Flowers'. Press and hold on any cell allows that cell to be moved, so this part works ok.
I want to restrict it so that I can only move in the first section.
Also, if I'm dragging a cell and move it from one section to another, it gives an error and the program crashes. I'd like it just to reject the move and send the cell back to its original position.
Thanks for any help. Ian
import UIKit
import MobileCoreServices
var fruitList = ["Orange", "Banana", "Apple", "Blueberry", "Mango"]
var flowerList = ["Rose", "Dahlia", "Hydrangea"]
// this all works
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self
tableView.dragInteractionEnabled = true
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 {
return "Fruit"
} else {
return "Flowers"
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return fruitList.count
} else {
return flowerList.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
if indexPath.section == 0 {
cell.textLabel?.text = fruitList[indexPath.row]
} else {
cell.textLabel?.text = flowerList[indexPath.row]
}
return cell
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
var string: String
if indexPath.section == 0 {
string = fruitList[indexPath.row]
} else {
string = flowerList[indexPath.row]
}
guard let data = string.data(using: .utf8) else { return [] }
let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)
return [UIDragItem(itemProvider: itemProvider)]
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}
}
Just in case anyone finds their way here, thanks to the link #Paulw11 has provided, this is the extra bit of code needed. Replace
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}
with
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let string = fruitList[sourceIndexPath.row]
fruitList.remove(at: sourceIndexPath.row)
fruitList.insert(string, at: destinationIndexPath.row)
}
Obviously this is just a simple example program, but I've now adapted this code to a complex situation and it works perfectly.
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:
I am working on a todo list app and whenever I run it on the simulator and try to print the items in my array, the other cells item get printed.
Here's my code:
import UIKit
class TodoListViewController: UITableViewController {
let itemArray = ["math","english"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// DATASOURCE METHODS
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ToDoItemCell", for: indexPath)
cell.textLabel?.text = itemArray[indexPath.row]
return cell
}
// DELEGATE METHODS
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
print(itemArray[indexPath.row])
}
}
You need didSelectRowAt not didDeselectRowAt , the latter is triggered when you select a row so you get the print from the previous selected row
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(itemArray[indexPath.row])
}
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?
Question 1)
I've spent a few hours trying to figure out this problem, from what I've read those are the two functions needed to implement the UITableView but it still gives me the error in the title:
import UIKit
class UITableView: UIViewController, UITableViewDataSource,UITableViewDelegate
{
override func viewDidLoad()
{
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
}
}
Question 2)
The TableView is a part of a tabbed view controller that I am implementing for my app. I want my TableView controller to have entries that once pressed will open another ViewController. How can I implement something like that?
Thank you in advance
You have incorrectly set up your ViewController. Either create a UITableViewController which is just a UITableView, or add a UITableView to a UIViewController (Like you have almost done).The didSelectRowAt method will allow you to segue to a new viewController, but in my examples only if it is set up correctly in a storyboard.
UITableViewController option
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell...
return cell
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
performSegue(withIdentifier: "SegueIdentifier", sender: self)
}
UIViewController option (The UITableView will need to be added through a Storyboard in this example)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell...
return cell
}
// MARK: - Table view delegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
performSegue(withIdentifier: "SegueIdentifier", sender: self)
}
Here is an example project to work from
You shouldn't be using UITableView to manage the data source. That should be the job of UITableViewController which manages data to be displayed on UITableView. The View in general should only worry about how to display the data, not manage it. See MVC design pattern by Apple.
Apple has a really nice, and complete Swift App tutorial (See Display the Data) that goes through how to set up UITableViewController with a table view you built with storyboard. You can also download and run the source project at the end of the page. I highly recommend it.
You have to to Implement This Mendatory DataSource Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "", for: indexPath)
return cell
}
Hope it Helps.