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.
Related
I have a table view inside which I am calling multiple nib as row under section
// MARK:- EXTENSIONS TABLE VIEWS
*
extension HomeController: UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func numberOfSections(in tableView: UITableView) -> Int {
return 9
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
let headerCell = tableView.dequeueReusableCell(withIdentifier: TableCellConstants.hHeaderCell) as! HomeHeaderCell
headerCell.viewAllBtn.tag = section
headerCell.viewAllBtn.addTarget(self, action: #selector(self.viewAll), for: .touchUpInside)
headerView.addSubview(headerCell)
return headerView
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 4{
guard let cell = self.productTableView.dequeueReusableCell(withIdentifier: TableCellConstants.trendingProductCell, for: indexPath) as? TrendingCell else{
return UITableViewCell()
}
return cell
}
else if indexPath.section == 5{
guard let cell = self.productTableView.dequeueReusableCell(withIdentifier: TableCellConstants.featureCell, for: indexPath) as? FeatureBrandCell else{
return UITableViewCell()
}
return cell
}else if indexPath.section == 6{
guard let cell = self.productTableView.dequeueReusableCell(withIdentifier: TableCellConstants.spotlightCell, for: indexPath) as? StoplightTableCell else{
return UITableViewCell()
}
return cell
}
guard let cell = self.productTableView.dequeueReusableCell(withIdentifier: TableCellConstants.momentCell, for: indexPath) as? PriviewProductCell else{
return UITableViewCell()
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 4{
return 310
}
else if indexPath.section == 5{
return 200
}
else if indexPath.section == 6{
return 320
}
else{
return 210
}
}
}
this is button action
#objc func viewAll(sender: UIButton){
print(sender.tag)
}
when I click on view all Button only first two section is working that means output is only 0 and 1 tags, remaining section button action not even working, I even put breakpoint nothing is coming on it, not only this even horizontal scroll is not working on collection view that I have under table view which
is under scroll view
I'm am not sure to what you did wrong in your code, however I created a test project to try and sort your issue out and I believe I have managed to do so.
My best guess at where you went wrong was either something to do with the UIButton's connection being made from interface builder to code. Or to do with how you specified which cells should be at which row and in which section, inside of the cellForRowAt method (I'll look at your code in more detail and update this if I find why your code was not working).
Heres the code in its entirety which worked for me:
View Controller:
NOTE: I changed the if statement blocks to a switch statement as I
thought it would make it easier to read, also as I did not have the
xib files you were using for the cells I created my own basic header
cell, I then used the basic preset table view cell for the cells
contained within each section. If you have not worked with switch
statements before, then I recommend you check out the section on them
in the swift language guide book:
https://docs.swift.org/swift-book/LanguageGuide/ControlFlow.html#ID127
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
// MARK:: Life-Cycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(HeaderTableViewCell.nib(), forCellReuseIdentifier: HeaderTableViewCell.id)
}
// MARK: Button Actions
#objc func viewAll(sender: UIButton){
print(sender.tag)
}
// MARK: Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func numberOfSections(in tableView: UITableView) -> Int {
return 9
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
let headerCell = tableView.dequeueReusableCell(withIdentifier: HeaderTableViewCell.id) as! HeaderTableViewCell
headerCell.viewAllBtn.tag = section
headerCell.viewAllBtn.addTarget(self, action: #selector(viewAll), for: .touchUpInside)
headerView.addSubview(headerCell)
return headerView
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 4:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Section 4"
return cell
case 5:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Section 5"
return cell
case 6:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Section 6"
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Unknown Section (Default)"
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch indexPath.section {
case 4:
return 310
case 5:
return 200
case 6:
return 320
default:
return 210
}
}
}
HeaderTableViewCell Cell xib
HeaderTableViewCell Code
class HeaderTableViewCell: UITableViewCell {
#IBOutlet var headerTitleLabel: UILabel!
#IBOutlet var viewAllBtn: UIButton!
static let id = "HeaderTableViewCell"
static func nib() -> UINib {
return UINib(nibName: "HeaderTableViewCell", bundle: nil)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
NOTE: Please also be aware that it probably would be a better idea for you to of just used a custom UIView instead of a custom UITableViewCell for your table view headers.
As i said I had collection view as row inside table view and that table view inside scroll view, so at last I removed scroll view from main view and kept all UI under table view now it working fine
Swift compiler get confused when we have three scrolling view working together so better avoid this, it is not best approach
Thanks #demented07 for your efforts
I have a button in my table and when it was click it will trigger the selected table index. for example when i click the button at a selected index it will trigger the code below. Thank You
code
func indexWasPressed(cell: ToDoListTableViewCell) {
trigger it here
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You selected cell #\(indexPath.item)!")
Use below code to get index from TableViewCell:
func indexWasPressed(cell: ToDoListTableViewCell) {
let indexPath = yourTableView.indexPath(for: cell)
let index = indexPath.row
}
try this:-
#objc func buttonPressed(sender: UIButton) {
// printing selected row
print("index path: \(sender.tag)")
//do you want section try this
var section = Int()
let pointInTable: CGPoint = sender.convert(sender.bounds.origin, to: self.tblView)
let cellIndexPath = self.tblView.indexPathForRow(at: pointInTable)
section(cellIndexPath!)
section = cellIndexPath!.row
print(section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cell for rows in \(indexPath.section)")
let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifire", for: indexPath) as! yourcell
cell.yourbutton.tag = indexpath.row
cell.yourbutton.addTarget(self, action: #selector(buttonpressed(sender:)), for: UIControlEvents.touchUpInside)
}
I have a view controller in which i have used table view controller in it. In my cell there is a button on which when user click the cell size should come to 550 and when click again it should come back to its original height. I have tried bit code after searching for it but it isn't working is their any solution that can work for me?. My code is bit this,
var indexOfCellToExpand: Int!
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == indexOfCellToExpand {
return 170 + expandedLabel.frame.height - 38
}
return 170
}
Use Auto layout for Expand-collapse Cell,
Attaching Demo for that case
link:https://www.dropbox.com/s/ieltq0honml35l8/TAbleDemo.zip?dl=0.
set cell button height constraint and update constraint on Value on select- deselect event and just reload data
in main viewcontroller code
self.tblView.estimatedRowHeight = 100
self.tblView.rowHeight = UITableViewAutomaticDimension
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TblCell", for: indexPath) as! TblCell
cell.btnExpandCollepse.addTarget(self, action: #selector(ViewController.expandCollapse(sender:)), for: .touchUpInside)
return cell
}
#objc func expandCollapse(sender:UIButton) {
self.tblView.reloadData()
}
Code in Cell Class
#IBOutlet var btnExpandCollepse: UIButton!
#IBOutlet var constraintBtnHeight: NSLayoutConstraint!
#IBAction func onExpandCollepse(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
if !sender.isSelected{
self.constraintBtnHeight.constant = 50
}else{
self.constraintBtnHeight.constant = 500
}
}
for constraint check below image
http://prntscr.com/hur8ym
Updated Demo for custom Content height of Lable with Autoresizing Cell
https://www.dropbox.com/s/o742kflg5yeofb8/TAbleDemo%202.zip?dl=0
https://www.dropbox.com/s/o742kflg5yeofb8/TAbleDemo%202.zip?dl=0
Swift 4.x
fileprivate var expandedIndexSet = Set<IndexPath>()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if expandedIndexSet.contains(indexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL_EXPANDED", for: indexPath) as! CellExpanded
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL_COLLAPSED", for: indexPath) as! CellCollapsed
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
if expandedIndexSet.contains(indexPath) {
expandedIndexSet.remove(indexPath)
} else {
expandedIndexSet.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .fade)
}
So I have a mainTableView of type ExpyTableView (https://github.com/okhanokbay/ExpyTableView). All fine , i managed to implement it and make it work but as it can be seen in the gif below i need to make +1 extra action for DidSelect and DidDeselect.
The thing is i want when selected to highlight with the green color and immediately expand the other rows , and when clicked on it right after to deselect and make the row back to normal . So normally this need to happen only after 2 taps on the screen... instead i make 4 as seen in the gif.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//MARK : Print
print("Selected Section \(indexPath.section), Row : \(indexPath.row)")
///////////////
if let cell = tableView.cellForRow(at: indexPath) {
if (indexPath.section == 1) || (indexPath.section == 2) || (indexPath.section == 3) {
cell.layer.borderWidth = 2.0
cell.layer.borderColor = Theme.defaultColor().cgColor
cell.layer.shadowOffset = CGSize.init(width: 0.5, height: 0.5)
}
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
tableView.deselectRow(at: indexPath, animated: false)
cell.layer.borderWidth = 0.1
cell.layer.borderColor = UIColor.lightGray.cgColor
}
}
Where did I go wrong?
Basically you listen to table changes for didDeselectRow which means following:
The code is executed once the table view cell is deselected. So you can forget about that code unless you really need it. However what you did is that you selects the row twice. Once for expansion, secondly to collapse.
What you need to do is to remember the index of selected cell and then deselect it while id didSelectRowAt
So declare a property fileprivate var currentlySelectedCellIndexPath: IndexPath? and then in didSelectRowAt use following.
var currentlySelectedCellIndexPath: IndexPath?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let currentlySelectedCellIndexPath = currentlySelectedCellIndexPath {
// unselect the selected one
makeCellUnSelected(in: tableView, on: currentlySelectedCellIndexPath)
guard currentlySelectedCellIndexPath != indexPath else {
tableView.deselectRow(at: currentlySelectedCellIndexPath , animated: true)
self.currentlySelectedCellIndexPath = nil
return
}
}
// Highlight the proper cell
makeCellSelected(in: tableView, on: indexPath)
currentlySelectedCellIndexPath = indexPath
}
func makeCellSelected(in tableView: UITableView, on indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
if (indexPath.section == 1) || (indexPath.section == 2) || (indexPath.section == 3) {
cell.layer.borderWidth = 2.0
cell.layer.borderColor = Theme.defaultColor().cgColor
cell.layer.shadowOffset = CGSize.init(width: 0.5, height: 0.5)
}
}
}
func makeCellUnSelected(in tableView: UITableView, on indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.layer.borderWidth = 0.1
cell.layer.borderColor = UIColor.lightGray.cgColor
}
}
To explain what it does:
The functions makeCellselected & makeCellUnselected just apply the style changes to highlight / unhighlight the cell as you did previously.
in the function func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) just check for currently visible and selected cell. If it is the same cell, then collapse it in guard and return, since you require only to expand / collapse. If it is not the same cell, then just discard the selection by calling makeCellUnselected and select the new one. This should solve your problem so far. If you have any other questions, just let me know.
What I would do is to have a property in your model isExpanded that will store if the cell needs to be expanded. That way you can use it in heightForRowAtIndexPath where you will get the item and read the isExpanded property.
I have a transaction tableview with different types of expenses that expands to show more detail when selected.
However the detail appears to be overwritten when this happens. I can see it flash and sometimes it does get populated. The textfields get populated correctly. I have been trying to debug this for a while, but not sure how to work around this problem.
Here is my current implementation:
// MARK: Tableview
extension TransactionViewController: UITableViewDelegate, UITableViewDataSource {
// MARK: - Table View
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CardCell", for: indexPath) as! CardTableViewCell
cell.delegate = self
var isDetailHidden = true
if indexPath.row == rowSelected {
isDetailHidden = false
}
let transaction = transactionList[indexPath.row]
cell.configureCell(transaction: transaction, isDetailHidden: isDetailHidden)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return transactionList.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == rowSelected {
// don't refresh and set again.
return
}
rowSelected = indexPath.row
transactionBeingEdited = transactionList[indexPath.row]
transactionTableView.setContentOffset(CGPoint(x: 0, y: rowSelected! * 76), animated: true)
let cell = tableView.dequeueReusableCell(withIdentifier: "CardCell", for: indexPath) as! CardTableViewCell
cell.delegate = self
cell.configureDetailCell()
transactionTableView.reloadData()
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let transaction = transactionList[indexPath.row]
coreDataManager.deleteTransaction(transaction: transaction)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row != rowSelected {
return 76.5
} else {
return 323
}
}
The variables in the detail section are dropdown boxes using a library. I've configured it in the UITableViewCell class. Setting up the dropdown methods occurs in the awakefromnib method.
private func setupBillDropDown() {
billDropDown.anchorView = bill
billDropDown.bottomOffset = CGPoint(x: 0, y: bill.bounds.height)
billDropDown.dataSource = TransactionType.list
// Action triggered on selection
billDropDown.selectionAction = { [unowned self] (index, item) in
self.bill.setTitle(item, for: .normal)
self.bill.setTitleColor(UIColor.white, for: .normal)
// Update transaction
if let transactionBeingEdited = self.delegate?.transactionBeingEdited {
transactionBeingEdited.type = item
self.coreDataManager.saveToCoreData()
self.coreDataManager.nc.post(name: .transactionBeingEdited, object: nil, userInfo: nil)
}
}
}
Thanks in advance.
I think in func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) you are overwriting the configured cell by loading the table. Try this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == rowSelected {
// don't refresh and set again.
return
}
rowSelected = indexPath.row
transactionBeingEdited = transactionList[indexPath.row]
transactionTableView.setContentOffset(CGPoint(x: 0, y: rowSelected! * 76), animated: true)
//let cell = tableView.dequeueReusableCell(withIdentifier: "CardCell", for: indexPath) as! CardTableViewCell
let cell = tableView.cellForRow(at: indexPath)
cell.configureDetailCell()
tableView.reloadRows(at: [indexPath], with: .none)
}
I figured out the issue after spending literally hours on it... It's because I was updating the buttons title using
bill.titleLabel!.text = transaction.type ?? "Select Bill"
insteaad of
bill.setTitle(transaction.type ?? "Select Bill", for: .normal)