How to implement the multiple selection in uitable in swift3? - ios

currently, i have added a long press gesture to my table view. It is working fine. Now the thing i want is that if i long press any UITableview cell that cell should get selected and after this if i tap on next cells that too should get selected too.
Below is the code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
return cell
}
#IBAction func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}

You can set the tableView's allowsMultipleSelection property in your longPress method. Since the longPress won't trigger the cell's selection you can you can use the gesture's location in the tableView to get the initial cell that corresponds to the longPress action.
func longPress(sender:UILongPressGestureRecognizer) {
switch sender.state {
case .began:
tableView.allowsMultipleSelection = true
let point = sender.location(in: tableView)
selectCellFromPoint(point: point)
default:break
}
}
func selectCellFromPoint(point:CGPoint) {
if let indexPath = tableView.indexPathForRow(at: point) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
}
}

Related

TableView Cell doesn't scale equally along y-axis

I have tableView cells that are populated with a color in each cell. What I want is when the user taps on the cell, it "opens"/expands so that that color fills the entire screen. Currently, it only scales downwards from the cell that I click on. I also need it to scale upwards along the y-axis, each cell expanding to the top of the screen, but I'm not sure what's prohibiting it to.
let expandedColorView: UIView = {
let view = UIView()
return view
}()
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.paletteTableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 1.0, animations: {
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
} )
}
}
}
}
UITapGestureRecognizer is declared in tableView(cellForRowAt:) with cell.isUserInteractionEnabled = true enabled.
I've tried changing the expandedColorView bounds self.expandedColorView.bounds.size.height = UIScreen.main.bounds.height in UIView.animate but that doesn't change anything. I was thinking the cell's frame would need to change so that it matches the parent view frame (which I think would be tableView) but I couldn't figure out how to do that.
Any help would be appreciated!
I've attached a gif of the issue:
If that's what you want
This is what I have done in data source extension
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "colorCell") as? ColorFulTableViewCell {
let color = colors[Int(indexPath.row % 7)]
cell.backgroundColor = color
return cell
}
return UITableViewCell()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let row = expandCell?.row, row == indexPath.row {
return self.tableView.bounds.size.height
}
else {
return 100
}
}
}
And tableView delegate extension looks like
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Whats happening her?
In heightForRowAt of TableView data source method, I check if cell need to cover the whole tableView size by using if let row = expandCell?.row, row == indexPath.row { and set its height to match the tableView height by returning self.tableView.bounds.size.height else I return 100
In didSelectRowAt I update the indexPath of cell to expand by saving it in expandCell and I reload the row (so that this time when height for row is called it can return self.tableView.bounds.size.height and I also call scrollToRow(at with position as .top to ensure my cell scrolls to top and makes itself visible completely
Because you are reloading only a specific cell, though from cost perspective its efficient, but animation might look rusty as other cells in visible indexPath array are adjusting them selves abruptly, you can always call reload Data to get much better smoother experience.
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadData()
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Hope this helps

Set Selected/ Unselected view color in UITableView in Swift

I have a tableview with which all views will be of clear colour. When user selected a cell I need to make selected tableview cell red and reset all other previous cells to clear colour.
How can I manage state of the cell whether it is selected or not.
I am using this code to change the color of selected index.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell:TableViewCell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
cell.backgroundColor = UIColor.red
}
I am not able to reset the previous cell.
You need to override setSelected(_:animated:) method in TableViewCell and configure the backgroundColor based on selected state.
class TableViewCell: UITableViewCell {
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.backgroundColor = selected ? .red : .clear
}
}
No need to change the backgroundColor in tableView(_:didSelectRowAt:) method.
You can use deselectRow func of tableView :
func deselectRow(at indexPath: IndexPath,
animated: Bool){
guard let cell:TableViewCell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
cell.backgroundColor = UIColor.clear
}
Hope it helps...
Based on PGDev's answer you need a property in the view controller to keep the selected index path
var selectedIndexPath : IndexPath?
If no row is selected the property is nil
In cellForRow add a line to manage the selection
cell.isSelected = indexPath == selectedIndexPath
In didSelectRowAt compare the just selected index path with selectedIndexPath, update selectedIndexPath and reload the rows accordingly.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var pathsToReload = [indexPath]
if let selectedPath = selectedIndexPath {
if indexPath == selectedPath { // deselect current row
selectedIndexPath == nil
} else { // deselect previous row, select current row
pathsToReload.append(selectedPath)
selectedIndexPath = indexPath
}
} else { // select current row
selectedIndexPath == indexPath
}
tableView.reloadRows(at: pathsToReload, with: .automatic)
}

UITableView How to call a function after reordering cells

In my project I wanted to implement moving rows as well as deleting them but not with stock "Delete" button but by tapping the image that is within my custom UITableViewCell called QueueCell. I delete rows in function deleteByTap2 which uses the sender.tag (which is the cell.indexPath.row) to recognise which cell should be removed. Both moving and deleting work great on their own but when you move, for example, 6th row to 2nd it still carries the tag = 6 and because of that when I tap on image to delete the row, incorrect row gets deleted. I created a function reTag which is supposed to update tags of all cells within the sections and it works great after being called in deleteByTap2 function but when called at the end of
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
it seems to not know the state of tableView after moving row. I searched the forum and I found that there was undocumented UITableViewDelegate function
- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath;
but I tried calling it and it seems it was removed (or maybe name changed)
When should I call the reTag function so it would work properly? So it would know the order of tableView after reordering it?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! QueueCell
let item = sungs[indexPath.section].songsIn[indexPath.row]
cell.setup(item: item)
if indexPath.section == 2{
let tap = UITapGestureRecognizer(target: self, action: #selector(deleteByTap2(_:)))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
cell.artwork.addGestureRecognizer(tap)
cell.artwork.isUserInteractionEnabled = true
cell.artwork.tag = indexPath.row
}
return cell
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
var toMove: MPMediaItem
tableView.beginUpdates()
if sourceIndexPath.section == 2{
if player.isShuffle{
toMove = player.shufQueue[player.shufIndex + sourceIndexPath.row + 1]
player.shufQueue.remove(at: player.shufIndex + sourceIndexPath.row + 1)
if destinationIndexPath.section == 2{
player.shufQueue.insert(toMove, at: player.shufIndex + destinationIndexPath.row + 1)
}
}else{
toMove = player.defQueue[player.defIndex + sourceIndexPath.row + 1]
player.defQueue.remove(at: player.defIndex + sourceIndexPath.row + 1)
if destinationIndexPath.section == 2{
player.defQueue.insert(toMove, at: player.defIndex + destinationIndexPath.row + 1)
}
}
}
tableView.endUpdates()
reTag(section: destinationIndexPath.section)
}
//the beginUpdates()-endUpdates() doesn't do much good here, actually it messes some of my cells
func reTag(section: Int){
var indexPath: IndexPath
for row in 0 ..< tableView.numberOfRows(inSection: section){
indexPath = IndexPath(row: row, section: section)
if let cell = tableView.cellForRow(at: indexPath) as? QueueCell{
cell.artwork.tag = row
}
}
}
func deleteByTap2(_ sender: UITapGestureRecognizer){
let tag = (sender.view?.tag)!
if player.isUsrQueue{
player.usrQueue.remove(at: player.usrIndex + tag + 1)
player.usrQueueCount! -= 1
sungs[2].songsIn.remove(at: tag)
}else{
player.defQueue.remove(at: player.defIndex + tag + 1)
sungs[2].songsIn.remove(at: tag)
player.defQueueCount! -= 1
}
let indexPath = IndexPath(row: tag, section: 2)
tableView.deleteRows(at: [indexPath], with: .fade)
reTag(section: 2)
}
This is a good example of why it's a bad idea to use .tag on objects to try and track them in this way.
I'd suggest that you move the tap gesture inside your cell class, and add a "call back" closure. This sample is, of course, missing your data class and cell.setup() code, but you should be able to see what needs to be changed:
// cell class
class QueueCell: UITableViewCell {
#IBOutlet weak var artwork: UIImageView!
var tapCallback: ((QueueCell) -> ())?
func addTap() {
if artwork.gestureRecognizers == nil {
// cells are reused, so only add this once
let tap = UITapGestureRecognizer(target: self, action: #selector(artworkTap(_:)))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
artwork.addGestureRecognizer(tap)
artwork.isUserInteractionEnabled = true
}
}
func artworkTap(_ sender: UITapGestureRecognizer) -> Void {
tapCallback?(self)
}
}
// table view class
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// all you have to do is manage your data,
// no need to reload() or "re-tag" anything
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "QueueCell", for: indexPath) as! QueueCell
// your cell configuration
//let item = sungs[indexPath.section].songsIn[indexPath.row]
//cell.setup(item: item)
if indexPath.section == 2 {
// tell the cell to add the gesture recognizer
cell.addTap()
// set the "call back" closure
cell.tapCallback = {
theCell in
if let iPath = tableView.indexPath(for: theCell) {
self.deleteByTap2(tableView, indexPath: iPath)
}
}
}
return cell
}
func deleteByTap2(_ tableView: UITableView, indexPath: IndexPath) -> Void {
print("Tapped on artwork at:", indexPath)
// you now have a reference to the table view and the indexPath for the cell that
// contained the artwork image view that was tapped
}
Now your deleteByTap2() function will match the familiar didSelectRowAt function, and you can handle your deleting there.

Deselecting custom UITableViewCell that was selected by UILongPressGestureRecognizer

I have a custom UITableViewCell class My custom UITableViewCell
If user wants to select first cell, UILongPressGestureRecognizer called after that cell selected and when user had already chosen minimum one cell, then he could choose another cells without long pressing on it, just calling function didSelectRow. I tried do it by myself, but I couldn't. It was actually working but first user that was selected by long press, can't be deselected. I searched and found that I should cancel touches in view, but it didn't work for me. So cells that are called with didSelectRow are working fine, I can select and deselect, except one cell that was selected by UILongPressGesture.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ContactCell {
if inSelectionMode {
selectUser(cell: cell, indexPath: indexPath)
} else {
if let cameraViewControl = presentingViewController as? CameraViewController {
cameraViewControl.smallView()
}
dismiss(animated: true, completion: nil)
}
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if tableView == contactTblView {
let tap = UILongPressGestureRecognizer(target: self, action: #selector(longTapSelectUser(_:)))
tap.cancelsTouchesInView = false
cell.addGestureRecognizer(tap)
}
}
func longTapSelectUser(_ gesture: UILongPressGestureRecognizer) {
if let cell = gesture.view as? ContactCell, let indexPath = self.contactTblView.indexPath(for: cell) {
inSelectionMode = true
selectUser(cell: cell, indexPath: indexPath)
}
}

Issue with UILongPressGestureRecognizer while using it in table view cell

I am implementing a long press in the uitableview through storyboard in swift3. I have only one prototype cell set in the storyboard. But the problem is the long press is being detected only in the first cell. Rest of the cells are not listening to the long press gesture.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
return cell
}
#IBAction func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}
The warning shown in the console is:
at a time, this was never allowed, and is now enforced. Beginning with iOS 9.0 it will be put in the first view it is loaded into.
Attach the gesture to the tableview, and when the gesture is triggered figure out which indexPath was selected.
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress(_:)))
tableView?.addGestureRecognizer(longPressRecognizer)
}
func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
let point = guesture.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: point);
print("Long Press \(String(describing: indexPath))")
}
}
Because a tableview is a kind of scrollview it is best to attach the gestures to the tableview itself and not any of its subview. This way it is less likely to interfere with the other gestures that must be tracked.
You need to add gesture for all cell in cellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let row = indexPath.row
cell.textLabel?.text = "Label"
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(HomeViewController.longPress(_:)))
cell?.addGestureRecognizer(longPressRecognizer)
return cell
}
func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
print("Long Press")
}
}

Resources