I have a ViewController with a UITableView (and one type of UITableViewCell) set up through Interface Builder. I hooked that up to some IBOutlets:
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.allowsSelection = false
tableView.dataSource = self
tableView.register(CommentTableViewCell.self, forCellReuseIdentifier: CommentTableViewCell.identifier)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 150
}
Then I implemented the following methods:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(self.comments.count)
return self.comments.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(self.comments[indexPath.row], indexPath.row)
let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.identifier, for: indexPath) as! CommentTableViewCell
cell.comment = self.comments[indexPath.row]
print(cell)
return cell
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
let comment = comments[indexPath.row]
return comment.user.isMe ? .delete : .none
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete, !isDeleting else {
return
}
isDeleting = true
let comment = self.comments[indexPath.row]
comment.delete() { result in
self.isDeleting = false
switch result {
case .success:
self.comments.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
case .failure:
self.displayError("Could not delete comment")
}
}
}
Now this seems simple and all (print statements for debugging purposes), however, the tableView won't actually display any cells. After tableView.reloadData() is called, the numberOfRowsInSection method returns 2. The cellForRowAt also prints 2 CommentTableViewCell's.
These are however not displayed. After some testing, I found out that the tableview itself however, is displayed.
Now why is it possible that this happens? I don't think I missed anything. Does anyone have experience with this?
Thanks :-)
Assuming the CommentTableViewCell has this comment label, make sure the vertical constraints for the label are set correctly. The problem seems to be about the incorrect cell height.
As suggested by Reinier Melian, try out fixed heights to see if that works.
If not, temporarily change the cell to a UITableViewCell, and set the title label instead and make it work. That way, we can isolate the problem better.
Related
I am using JSON to parse data from Spotify and add songs into a UITableView. The songs play fine, and I added functionality for deleting cells, but when adding functionality for reording cells, I can''t play songs and I can't swipe to delete them either. Any ideas would be appreciated.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.tableView.isEditing = true
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
This adds the album image and song name to the TableView.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
let mainImageView = cell?.viewWithTag(2) as! UIImageView
mainImageView.image = posts[indexPath.row].mainImage
let mainLabel = cell?.viewWithTag(1) as! UILabel
mainLabel.text = posts[indexPath.row].name
return cell!
}
This adds the swipe to delete functionality.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
posts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .none
}
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
This adds the reordering functionality.
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movedObject = self.posts[sourceIndexPath.row]
posts.remove(at: sourceIndexPath.row)
posts.insert(movedObject, at: destinationIndexPath.row)
debugPrint("\(sourceIndexPath.row) => \(destinationIndexPath.row)")
self.tableView.reloadData()
}
You don't want to set
self.tableView.isEditing = true
in viewDidLoad. This takes you from the "normal" mode where you can select a cell, or other elements in a cell. Setting "self.tableview.isEditing" is the equivalent of hitting an edit button on the top right-hand corner of many tableViews.
TableView leaving blank cells
Repeating blank cells
When deleting rows from the bottom
After deleting a row or multiple rows in my TableView, the TableView Cells seems to shift or refresh in an odd way that creates multiple blank rows. Seems to start with rows that are off-screen.
I have tried using beginUpdates, endUpdates, and performBatchUpdates with no change in behavior. I have also confirmed that the data source array is being updated properly and so is the number of rows in the tableview.
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return paymentsArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserPaymentCell
let payment = paymentsArray[indexPath.row]
cell.payment = payment
cell.selectionStyle = .none
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets(top: 0, left: 75, bottom: 0, right: 0)
cell.layoutMargins = UIEdgeInsets.zero
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
let payment = paymentsArray[indexPath.row]
if payment.payerUID == Auth.auth().currentUser?.uid {
return true
} else {
return false
}
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let payment = paymentsArray[indexPath.row]
switch editingStyle {
case .delete:
deleteBillAndRefreshTotals(bill: payment, indexPath: indexPath)
default:
return
}
}
func deleteBillAndRefreshTotals(bill: Bill, indexPath: IndexPath) {
print("DELETING CELL")
paymentsArray.remove(at: indexPath.row)
paymentsTableView.deleteRows(at: [indexPath], with: .automatic)
print(paymentsTableView.numberOfRows(inSection: 0))
}
Expected results - for row to be deleted and all cells above or below the deleted cell to shift together.
override func prepareForReuse() {
super.prepareForReuse() // <--
self.nameLabel.text = nil
self.backgroundColor = .white
}
Within my custom cell implementation, the above function was being called without calling super.prepareForReuse first. Therefore causing the issues above.
after perform delete operations call reloaddata method so after that tableview will refresh.
func deleteBillAndRefreshTotals(bill: Bill, indexPath: IndexPath) {
print("DELETING CELL")
paymentsArray.remove(at: indexPath.row)
paymentsTableView.reloaddata()
print(paymentsTableView.numberOfRows(inSection: 0))
}
You can try this code :
paymentsTableView.beginUpdates()
paymentsTableView.deleteRows(at: [indexPath], with: .automatic)
paymentsTableView.endUpdates()
TableView CheckMark Cell Value Removed After Scrolling Up It will Fix
TableView in You have face a problem many times to Checkmark after scroll Up then Scroll Down To show a Your Checkmark cell is will Removed Because cell is dequeueReusableCell So This Problem Fix , you Have just put Your code and Solved Your Problem.
Any More Help So Send Massage.
Thank you So much. :)
class ViewController: UIViewController , UITableViewDataSource , UITableViewDelegate{
var temp = [Int]()
var numarr = [Int]()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numarr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "id")
cell = UITableViewCell.init(style: .default, reuseIdentifier: "id")
cell?.textLabel?.text = String(numarr[indexPath.row])
if temp.contains(numarr[indexPath.row] as Int)
{
cell?.accessoryType = .checkmark
}
else
{
cell?.accessoryType = .none
}
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
if temp.contains(numarr[indexPath.row] as Int)
{
cell?.accessoryType = .none
temp.remove(at: temp.index(of: numarr[indexPath.row])!)
}
else
{
cell?.accessoryType = .checkmark
temp.append(self.numarr[indexPath.row] as Int)
}
}
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...100
{
numarr.append(i)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
I think if someone were to run your code it would not show any error. But with real data it probably will. The reason is the way you store your checkmarks. You store the data of a row into the temp array when you should be storing the actualy indexPath of the array so that only that row gets the checkmark. In your case, if a row has 1 inside it's label and you click on it, that cell will be highlighted. Now if you start scrolling and another cell contains 1 then that row will also be highlighted.
I have modified your example for the case of a single section. If there is more than one section, you need to store the indexPath instead of indexPath.row.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "id")
cell = UITableViewCell.init(style: .default, reuseIdentifier: "id")
cell?.textLabel?.text = String(numarr[indexPath.row])
if temp.contains(indexPath.row) {
cell?.accessoryType = .checkmark
} else {
cell?.accessoryType = .none
}
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
if temp.contains(indexPath.row) {
cell?.accessoryType = .none
temp.remove(at: indexPath.row)
} else {
cell?.accessoryType = .checkmark
temp.append(indexPath.row)
}
}
You are strongly discouraged from using a second array to keep the selected state.
This is Swift, an object oriented language. Use a custom struct for both num and the selected state.
In didSelectRowAt and didDeselectRowAt change the value of isSelected and reload the row.
And use always the dequeueReusableCell API which returns a non-optional cell.
struct Item {
let num : Int
var isSelected : Bool
}
var numarr = [Item]()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numarr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "id", for: indexPath)
let item = numarr[indexPath.row]
cell.textLabel?.text = String(item)
cell.accessoryType = item.isSelected ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
updateSelection(at: indexPath, value : true)
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
updateSelection(at: indexPath, value : false)
}
func updateSelection(at indexPath: IndexPath, value : Bool) {
let item = numarr[indexPath.row]
item.isSelected = value
tableView.reloadRows(at: [indexPath], with: .none)
}
override func viewDidLoad() {
super.viewDidLoad()
(0...100).map{Item(num: $0, isSelected: false)}
}
I am making a music genre picking application and when I go to my table to select genres, I select a row and it selects a random row about 10 or so down from my selection.
My code for the selection is:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let genresFromLibrary = genrequery.collections
let rowitem = genresFromLibrary![indexPath.row].representativeItem
print(rowitem?.value(forProperty: MPMediaItemPropertyGenre) as! String
)
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .checkmark
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .none
}
}
Cells are reused by default when cellForRowAtIndexPath is called. This causes the cells to have the wrong data when you don't keep track of the indexPaths that have been selected. You need to keep track of the index paths that are currently selected so you can show the appropriate accessory type in your table view.
One way of doing it is to have a property in your UITableViewController that just stores the index paths of the selected cells. It can be an array or a set.
var selectedIndexPaths = Set<IndexPath>()
When you select a row on didSelectRowAt, add or remove the cell from selectedIndexPaths, depending on whether the index path is already in the array or not:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) {
// The index path is already in the array, so remove it.
selectedIndexPaths.remove(indexPathIndex)
} else {
// The index path is not part of the array
selectedIndexPaths.append(indexPath)
}
// Show the changes in the selected cell (otherwise you wouldn't see the checkmark or lack thereof until cellForRowAt got called again for this cell).
tableView.reloadRows(at: [indexPath], with: .none)
}
Once you have this, on your cellForRowAtIndexPath, check if the indexPath is in the selectedIndexPaths array to choose the accessoryType.
if selectedIndexPaths.contains(indexPath) {
// Cell is selected
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
This should solve the problem of the seemingly random cells that are checked every 10 cells down or so (which, is not random, it's just that the cell with the checkmark is being reused).
Because cellForRow returns a cached cell you generated. When scrolling out of the screen the order of cells are changed and cells are reused. So it seems "randomly selected".
Don use cellForRow, instead record selection data.
Here's code works in a single view playground.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
var selection: [IndexPath: Bool] = [:]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
view.addSubview(tableView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = self.view.bounds
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "c")
if let sc = cell {
sc.accessoryType = .none
let isSelected = selection[indexPath] ?? false
sc.accessoryType = isSelected ? .checkmark : .none
return sc
}
return UITableViewCell(style: .default, reuseIdentifier: "c")
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.textLabel?.text = NSNumber(value: indexPath.row).stringValue
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selection[indexPath] = true
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Not sure why but the top row of my uitableview is not editable, all other rows function as normal and delete as expected. It's like caneditrowat indexPath: Indexpath isn't working for that one row. See images attached.
My code in tableView(_:commit:forRowAt:) looks like all the tutorials I can find, can't seem to find any other examples with this problem.
//MARK: Properties
var favouriteExercises = [FavouriteExercise]()
override func viewDidLoad() {
super.viewDidLoad()
//Load exercises from local DB
if let savedFavouriteExercises = loadFavouriteExercises()
{
//loading exercises in from the favourites
favouriteExercises += savedFavouriteExercises
}
// Use the edit button item provided by the table view controller.
navigationItem.rightBarButtonItem = editButtonItem
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//count number of rows in table
return favouriteExercises.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "FavouriteTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? FavouriteTableViewCell else {
fatalError("The dequeued cell is not an instance of FavouriteTableViewCell.")
}
// Fetches the appropriate exercise for the data source layout.
let exercise = favouriteExercises[indexPath.row]
//setup the layout for the cell in the table view
cell.nameLabel.text = exercise.name
let url = URL(string: (exercise.iconUrl))!
cell.photoImageView.sd_setImage(with: url)
//cell.photoImageView.image = #imageLiteral(resourceName: "defaultPhoto")
cell.backgroundColor = UIColor.darkGray
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
favouriteExercises.remove(at: indexPath.row)
saveFavouriteExercisess()
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Thanks for the help
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle
{
return UITableViewCellEditingStyle.delete
}
Answer adapted from #KumarReddy's solution