I am getting JSON data from an API and display it in a tableView. When user scrolls at the last row, the next page will be added to the current data.
But I am getting this error when next page data is added.
[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray
func loadUser(_ currentPage:Int=1){
APIService.loadUser(currentPage, size: 100, callback: { data in
if let data = data["user"].arrayValue as [JSON]?{
self.jsonData?.append(contentsOf: data
self.tableView.reloadData()
self.hideLoadingInView(view: self.view)
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.jsonData?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell") as! UserTableViewCell
cell.user = self.jsonData?[indexPath.row]
if !isLoading && (indexPath.row == self.jsonData!.count - 1){
currentPage += 1
loadUser(currentPage)
}
return cell
}
I would suggest few potential fixes:
Add Exception Breakpoint to your project, so you can better debug your particular issue. It's very useful tip: https://www.natashatherobot.com/xcode-debugging-trick/ You'll probably find out what's wrong with this.
Move the UI manipulation inside the API call response into the DispatchQueue.main.async { ... } block, to be sure the UI is manipulated on the main thread. Otherwise it can cause weird behavior and maybe also your issue.
Move the check if you need to fetch another page from API to the tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) function. Some people do recommend to do it in scrollViewDidScroll delegated method of UIScrollView.
Then it will look like this.
func loadUser(_ currentPage:Int=1){
APIService.loadUser(currentPage, size: 100, callback: { data in
if let data = data["user"].arrayValue as [JSON]?{
DispatchQueue.main.async {
self.jsonData?.append(contentsOf: data)
self.tableView.reloadData()
self.hideLoadingInView(view: self.view)
}
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.jsonData?.count ?? 0
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if !isLoading && (indexPath.row == self.jsonData!.count - 1){
currentPage += 1
loadUser(currentPage)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell") as! UserTableViewCell
cell.user = self.jsonData?[indexPath.row]
return cell
}
Related
I have some problems with reloading section in tableView. If the cell is hiding a bit on the top, it becomes flicking. If the cell is fully visible, everything works fine. The height of cell is fixed, and it's not automaticDimension. All of cells are standard UITableViewCell with different background color. I know that tableView is a very common UI element in ios, and I hope someone had faced the same problem before. Thanks!
func numberOfSections(in tableView: UITableView) -> Int {
return contactsForSections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let contact = contactsForSections[section]
return contact.isExpanded ? contact.numbers.count : 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = UITableViewCell()
cell.contentView.backgroundColor = .green
return cell
} else {
let cell = UITableViewCell()
cell.contentView.backgroundColor = .red
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.row == 0 else { return }
contactsForSections[indexPath.section].isExpanded.toggle()
tableView.reloadSections([indexPath.section], with: .none)
}
I have a UITableViewController where I have cells that I want to hide.
What I'm currently doing is hiding the cells with heightForRowAt returning 0 and cellForRowAt returning a cell with isHidden = false. But since I am using this solution, I noticed the app was slower when I'm scrolling in my tableView.
// Currently returning a height of 0 for hidden cells
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let post = timeline?.postObjects?[indexPath.row], post.hidden ?? false {
return 0.0
}
return UITableView.automaticDimension
}
// And a cell with cell.isHidden = false (corresponding to identifier "hiddenCell")
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = timeline?.postObjects?[indexPath.row] {
if post.hidden ?? false {
return tableView.dequeueReusableCell(withIdentifier: "hiddenCell", for: indexPath)
} else {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
}
I was thinking about why not apply a filter on the array to totally remove hidden cells of the tableView, but I don't know if filtering them each time is great for performances...
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timeline?.postObjects?.filter{!($0.hidden ?? false)}.count
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = timeline?.postObjects?.filter{!($0.hidden ?? false)}[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
What is the best option? Hiding cells when generating them (first) or exclude them of the list (second)?
I would recommend to let the table view data source methods to deal with a filtered version of timeline. However, do not do this in cellForRowAt method because we need to do it one time but not for each cell drawing.
So, what you could do is to declare filteredTimeline and do the filter one time in the viewDidLoad method (for instance):
class TableViewController: UIViewController {
// ...
var filteredTimeline // as the same type of `timeline`
override func viewDidLoad() {
// ...
filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}
// ...
}
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredTimeline.count ?? 0
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = filteredTimeline?.postObjects?.filter{!($0.hidden ?? false)}[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
// ...
}
In case of there is a better place to filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)} rather than viewDidLoad, you might need to call tableView.reloadData().
An alternative you could do:
if you think that you don't need the original timeline you could filter it itself:
timeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}
tableView.reloadData()
and you will not need an extra filtered array.
Extra tip:
In case of returning 0.0 value in heightForRowAt method for a certain row, cellForRowAt will not even get called; For example:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return indexPath.row == 0 ?? 0.0 : 100.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// ...
}
At this point, cellForRowAt should get called only one time because the height for the first row is 0.0.
There is no point of having cells with a size of 0. Your best bet is to filter your datasource, but my suggestion would be to keep two arrays at the same time.
But handle the filtering elsewhere then in the numberOfRowsInSection.
var filteredObjects = []
func filterObjects() {
filteredObjects = timeline?.postObjects?.filter{!($0.hidden ?? false)}
}
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredObjects.count
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = filteredObjects[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
I don't know how you handle the filtering, but whenever you want to apply your filter you simply
filterObjects()
tableView.reloadData()
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()
I just create the Bus ticket booking app. Now i want to save the search history (City Name and Date) and Display in my tableView. So this is what i coding in my SearchButtonAction.
#IBAction func btnSearchAction(_ sender: Any) {
if txtTo.text != nil && txtFrom.text != nil {
arrTo.append(txtTo.text!)
arrFrom.append(txtTo.text!)
UserDefault.set(arrTo, forKey: "arrTo")
UserDefault.set(arrFrom,forKey: "arrFrom")...
}
}
Now on my historyViewController I just coding like below
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrFrom.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 200
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RecentCell") as! RecentCell
cell.selectionStyle = .none
cell.lblFrom.text = arrFrom[indexPath.row]
cell.lblTo.text = arrTo[indexPath.row]
tblSearch.reloadData()
return cell
}
So it's Call only numberOfRowsInSection Method again and again Even arr count is more than one. What is the correct way to save two textField text in UserDefaults.
Remove reloading datas of tblSearch from cellForRowAt TableView data source method
When you reload tableview inside cellForRowAt method, tableview will go into looping. So you tableview won't display. So please remove tblSearch.reloadData() this line from cellForRow methhod.
please check screenshot, so you will get the better idea about looping.
http://prntscr.com/lte2mo
I'm calling a TableView inside of a custom ViewController. In my UITableView, my cells overlap. Like so
In the storyboard editor, I have the Row Height set to Custom at 63pts, but they look like they're actually using the the default height that the cell would be if you unchecked the Custom box.
Like in some of the other solutions tried implementing the heightForRowAt function but it doesn't do anything. I've tried using obscenely large numbers, but the result is always exactly as it was before.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
if(self.view.traitCollection.horizontalSizeClass == .compact ||
self.view.traitCollection.verticalSizeClass == .compact) {
cellHeight = 55
return 55
}
else{
return 63
}
}
The only thing in solutions I've seen so far that looks at all promising is this one. iOS UITableViewCell overlapping
I've tried implementing the prepareForReuse Function in my custom TableViewCell class file, but I didn't know what I was actually supposed to add to the function, so I just called the super function and added a print statement
--- Full TableView Code ---
extension SavingViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "WishlistTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? WishlistTableViewCell else {
fatalError("The dequeued cell is not an instance of WishlistTableViewCell.")
}
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
if(self.view.traitCollection.horizontalSizeClass == .compact ||
self.view.traitCollection.verticalSizeClass == .compact) {
cellHeight = 55
return 55
}
else{
return 63
}
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
/*
let resultRemoving = bookmarks[indexPath.row]
if var bookmarkData = defaults.object(forKey:"bookmarkDict") as? [String: [String]] {
bookmarkData.removeValue(forKey: resultRemoving.resultURL)
defaults.set(bookmarkData, forKey:"bookmarkDict")
}
defaults.synchronize()
*/
items.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
}
}
}
Currently heightForRowAt is not being called since heightForRowAt method is a UITableViewDelegate method so
Your extension should look like this:
Add UITableViewDelegate
extension SavingViewController: UITableViewDataSource, UITableViewDelegate {
}