I've got a table view showing the output of a search. When I update it to show the output of a totally different search if the old set of results was longer then old cells remain below my new ones.
For examples, if my first results are:
[Sam,
Joe,
Sally,
Betty,
Bob]
then I have five cells, one per result, as expected. If my second set of results is short, say just
[Smith]
then I now have five cells (Smith, Joe, Sally, Betty and Bob), when only one (Smith) is expected.
Here's how I'm reloading:
results = getResults()
tableView.reloadData()
And here's how I'm getting the number of cells:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if results != nil {
println("Table has \(results!.count) rows.")
return results!.count
}
println("Table is empty.")
return 0
}
which is printing out "Table has 1 rows." as expected, but the four old rows are still there.
Now, I could delete them before reloading, or delete the whole section, but is there a better way of achieving this? I thought reloadData would reload everything.
Additional Info
Here's cellForRowAtIndexPath as requested:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchEventsCell", forIndexPath: indexPath) as SearchEventsCell
if results != nil && eventStore != nil && results!.count >= indexPath.row {
let event = results![indexPath.row] as EKEvent
cell.configureCellWithEvent(event)
}
else {
println("Couldn't dequeue the cell")
}
return cell
}
And just to prove we have the right number of rows I put a println in before reloadData():
println("We're about to reload the table view, we have \(numberOfSectionsInTableView(tableView)) sections and \(tableView(tableView, numberOfRowsInSection:0)) rows in section 0")
tableView.reloadData()
Which outputs
Table has 1 rows.
We're about to reload the table view, we have 1 sections and 1 rows in sections 0
Table has 1 rows.
as it should.
Something else I've noticed, which surely has to be related - the table doesn't update at all until I try scrolling. What am I missing? I know reloadData has been called as println is being called within numberOfRowsInSection.
Update
The textFieldShouldReturn method that triggers the update includes this code:
eventStore.requestAccessToEntityType(EKEntityTypeEvent,
{ accessGranted, error in
if accessGranted {
if let searchEventsController = self.searchEventsController {
searchEventsController.search(self.searchTextField.text)
}
}
else {
self.accessDenied()
}
}
)
which seems very likely to be the culprit. Is there a better way of checking for permission? I included it there so that if the user ever disallowed it it would ask again next time they try to use it, rather than just failing.
The problem was indeed the fact that reloadData was taking place in another thread due to the eventStore.requestAccessToEntityType call.
There are two solutions:
1) Perform the permissions check once, when the app loads, instead of every time you access the the EventStore, as suggested by Paulw11. This means for the majority of the application there's only one thread.
2) Use the following code to execute reloadData on the main thread:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
as suggested by almas.
Update: I've just checked and if you revoke the permission for the app to access the Calendar then it doesn't ask the user again anyway, it just denies access, so there's no reason to keep the eventStore.requestAccessToEntityType where it is.
Related
I have an UITableView, quite standard, with quite dynamic datasource (messages).
To add messages to UITableView I'm using insertRows.
func insertRowsInTableAtIndexPath(ip: IndexPath, right: Bool) {
print("INSERT ROW visible cells: \(tableView.visibleCells.count)\n dispatcher messages \(dispatcher.messages.count)\n numberOfRows \(tableView.numberOfRows(inSection: 0))\n numberOfSections \(tableView.numberOfSections)\n ip: \(ip.row) \(ip.section)")
if tableView != nil {
if right == true {
self.tableView.insertRows(at: [ip], with: UITableView.RowAnimation.right)
} else {
self.tableView.insertRows(at: [ip], with: UITableView.RowAnimation.left)
}
}
print("AFTER INSERT ROW visible cells: \(tableView.visibleCells.count)\n dispatcher messages \(dispatcher.messages.count)\n numberOfRows \(tableView.numberOfRows(inSection: 0))\n numberOfSections \(tableView.numberOfSections)\n ip: \(ip.row) \(ip.section)")
}
It looks pretty standard, because it is. The problem is that in some rare cases (I can replay them but it'd be quite difficult to describe them here) I'm getting the famous NSInternalInconsistencyException: Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (0) and so on.
In these cases the first message is successfully added to the dataSource, and it looks like this message also is inserted as row as well, but it doesn't show up on screen, and the next message is breaking my UITableView because it is invalide update (and it is).
I know that this is happening because of all of these print commands before and after inserting the cell. In bad case these numbers looks like that:
Before inserting the first message:
visible cells: 0
number of messages in dataSource: 1
numberOfRows: 0
numberOfSections: 1
ip: 0 0 (indexPath.row, indexPath.section)
After inserting the first message:
visible cells: 1
number of messages in dataSource: 1
numberOfRows: 1
numberOfSections: 1
ip: 0 0 (indexPath.row, indexPath.section)
At this point I can't see that cell (that was successfully inserted if I could believe UITableView.visibleCells.count)
But if I'm trying to insert the next message, I'm getting an exception. Moreover: UITableView.visibleCells.count is suddenly 0, and numberOfRows is 0 too, although I didn't remove previous row and I didn't remove this message from dataSource.
visible cells: 0
dispatcher messages 2
numberOfRows 0
numberOfSections 1
ip: 0 0
So, I have three questions:
Is it possible for UITableView to not to insert row with insertRows, and how could I prevent this behavior. Why the first row is not showing in the first place?
Why this first row is disappearing from numberOfRows and visibleCells after a while?
What am I doing wrong here?
I understand that this amount of code is not enough to solve this issue from scratch, but this UITableView is pretty standard, and dataSource logic is not so standard and relatively complicated, so my only hope is for people who already have experienced something like that.
update (dataSource methods):
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dispatcher.messages.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
update 2 (the most relevant part of the code):
//this should work when user is changing from one 'chat' to another
//so, first of all, I'm removing all the messages from the dataSource
dispatcher.messages.removeAll()
//also, I'm stopping the timer
if timer != nil {
timer.invalidate()
timer = nil
}
//also, I'm reloading the table with no messages
if tableView != nil {
tableView.reloadData()
}
//after that I'm starting the timer again, and trying to add new messages to the UITableView. The first message is going through (although it is not shown in the UITableView as it should be), the second message is throwing an error because the first row doesn't exist at this point).
let newMessage = dispatcher.giveMeNewMessage()
if newMessage != nil {
dispatcher.messages.insert(newMessage!, at: 0)
let ip = IndexPath(row: 0, section: 0)
insertRowsInTableAtIndexPath(ip: ip, right: false)
self.scrollToRow(ip: ip) //standard scrollToRow
}
My app uses pagination in order to display the data in the table. At every 5 recipes, I make a new request with Alamofire to get the data and display it in the cells. I also have a search bar which uses the same tableView to display the searched data in (and also uses pagination).
When I press the cancel button, the most recent recipes from the server will be displayed.
The problem is that if I am scrolling between two 'pages' and press the cancel button, the app would crash.
My guess is that this happens because the app will try to populate the cell at an index corresponding to a search result cell (which will be higher than the results from the first list).
The recipes from the search results and the ones from the main list are the same because it is a mock server (you can look at the console to better understand what requests are being made).
Video: https://www.youtube.com/watch?v=NGGZBZtY-J4&spfreload=10
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! RecipeTableViewCell
if searchController.isActive == false {
cell.loadRecipePreview(recipe: recipes[indexPath.row])
if indexPath.row == recipes.count - 1 {
recipeRequest()
}
}
else {
cell.loadRecipePreview(recipe: searchRecipes[indexPath.row])
if indexPath.row == searchRecipes.count - 1 {
recipeSearchRequest()
}
}
return cell
}
Edit: I think that the problem doesn't have anything to do with pagination, because this also happens when not scrolling between two pages.
Fixed by modfiying the first if with if searchController.isActive == false && indexPath.row < recipes.count && recipes.count != 0 altough I am not sure that this is the best way to do it. The question is still open.
How many recipes are in your array recipes[]? You are using an array index which is greater than the array size. Arrays are "zero-based", i.e if array has a total of 6 items, valid array indexes are 0...5. If you attempt to access recipes[7] you will get that "fatal: index out of range error" you are seeing in your log.
Make sure the number of rows correspond to the number of recipes in the array.
I've got an array of rooms I'm wanting to filter by usercount/topvotes sorta stuff.
I'm hitting buttons to trigger the filter changes and set a value to "CurrentSearch", reloading my tabledata, and in my cellForRowAtIndexPath I'm checking the current currentsearch value and filtering the list accordingly.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if currentSearch == "new" {
self.PersonalSearchesList = PersonalSearchesList.sorted{ $0.users < $1.users }
} else if currentSearch == "top" {
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.latestUser < $1.latestUser }
}
My images/labels load in perfectly fine before the filtering. On the initial load I'm setting currentSearch to "new" and it works perfectly. It's only once I start trying to swap between filters that things get funky. I'm getting random duplicates of cells, and while I'm swapping between the filters the incorrect loads stay consistent..as in the same duplicates of the same cells are being made/placed at the same spots.
I have it when I click on a cell the information for the cell is printed..and despite the cell information being loaded incorrectly the actual information is correct and the lists are ordered as they should be.
any ideas?
You should do something like this:
var currentSearch: String {
didSet {
// sort your array
tableView?.reloadData() // reload data
}
}
if you will sort your every then it every time get sorted when you scroll your tableview or reload it. so sort your data somewhere else in other method and after successfully sorting reload your tableview.
I'm new to IOS so forgive me for my coding mistakes. I'm facing an issue where I have a tableView Controller with two sections. The first section has a button, when clicked, appends data into an array and deletes it's own row in the first section (i did this as there are extra non related rows in the first section). The number of rows in the second section is based upon array.count.
My issue is that I tried begin/end update, and it still doesn't work. Whenever I run the code below and run the startNewDay function (when the button is clicked), this error occurs:
'attempt to insert row 0 into section 1, but there are only 0 rows in section 1 after the update'
This doesn't make any sense, as I appended the array already before I inserted the new rows. The array was empty before I appended it. Shouldn't there be the same number of rows in the second section as array.count?
Table View Delegate code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if dataModel.lists[0].dayHasStarted == false {
return 2
} else {
return 1
}
} else {
if itemDoneCount == dataModel.lists[0].item.count && dataModel.lists[0].doneButtonVisible {
return dataModel.lists[0].item.count + 1
} else {
return dataModel.lists[0].item.count
}
}
}
startNewDay button function when pressed:
#IBAction func startNewDayDidPress(sender: AnyObject) {
dataModel.lists[0].dayHasStarted = true
dataModel.lists[0].startDate = NSDate()
addItemButton.enabled = !addItemButton.enabled
// deleting start new day button
let indexPath = NSIndexPath(forRow: 1, inSection: 0)
let indexPaths = [indexPath]
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
// Inserting new array elements and rows into 2nd section
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
tableView.beginUpdates()
var insertRitualsArray = [NSIndexPath]()
for item in itemsArray {
let itemIndex = itemsArray.indexOf(item)
let indexPath = NSIndexPath(forRow: itemIndex!, inSection: 1)
insertRitualsArray.append(indexPath)
}
tableView.insertRowsAtIndexPaths(insertRitualsArray, withRowAnimation: .Top)
tableView.endUpdates()
}
SOLVED
The problem of this code is not at all related to the previous title of this thread, which may be misleading to people having the same issue as mine. Hence, I will be changing it. The previous title (for the curious) was :
"tableView.begin/end update not updating number of rows in section"
Just for others who might come across this issue, the issue isn't in the tableView delegate, nor is it in reloading the tableview data. For readability, I placed both dataModel.list[0].item into itemsArray and dataModel.list[0].item into ritualsArray. This apparently updates the itemsArray when appended but not the initial dataModel.list[0].item instead, which caused the second section in the tableView not to load the new number of rows, causing the error when inserting rows into non-existant rows.
Hence instead of:
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
this solved it:
dataModel.list[0].item += dataModel.list[0].rituals
Hope it helps any beginner like me out there that comes across this issue.
Latest update
I found out recently that an array is of value type, and not reference type. Hence placing an array into a variable makes a copy of that array instead of serving as a placeholder for the original array.
Beginner mistake opps.
The error you are receiving means that the datasource contains a different number of items to however many there would be after inserting or deleting rows. This probably means that the data are not being inserted into your datasource array, or that the data do not match the criteria in the if statements in your numberOfRowsInSection function. To troubleshoot this, you should log the contents of the datasource array after modifying it to check what its contents are. If they are what you are expecting (I.e. The data have been added correctly) then the issue is in the way you are evaluating its contents to establish the number of rows. If the contents are not what you are expecting, then the issue is in the way you are inserting the data into the datasource array.
I had a similar problem after deleting a row. It seems that if
numberOfRowsInSection is not coherent (equal to last value -1) this error appears.
I see that there's a condition in your numberOfRowsInSection, this is perhaps the culprit
This question already has answers here:
How to detect the end of loading of UITableView
(22 answers)
Closed 6 years ago.
I need to call a function after the UITableView has been loaded completely. I know that in most cases not every row is displayed when you load the view for the first time, but in my case, it does as I only have 8 rows in total.
The annoying part is that the called function needs to access some of the tableView data, therefore, I cannot call it before the table has been loaded completely otherwise I'll get a bunch of errors.
Calling my function in the viewDidAppear hurts the user Experience as this function changes the UI. Putting it in the viewWillAppear screws up the execution (and I have no idea why) and putting it in the viewDidLayoutSubviews works really well but as it's getting called every time the layout changes I'm afraid of some bugs that could occur while it reloads the tableView.
I've found very little help about this topic. Tried few things I found here but it didn't work unfortunately as it seems a little bit outdated. The possible duplicate post's solution doesn't work and I tried it before posting here.
Any help is appreciated! Thanks.
Edit: I'm populating my tableView with some data and I have no problems with that. I got 2 sections and in each 4 rows. By default the user only sees 5 rows (4 in the first section, and only one in the second the rest is hidden). When the user clicks on the first row of the first section it displays the first row of the second section. When he clicks on the second row of the first section it displays two rows of the second section, and so on. If the user then clicks on the first row of the first section again, only one cell in the second section is displayed. He can then save his choice.
At the same time, the system changes the color of the selected row in the first section so the users know what to do.
Part of my issue here is that I want to update the Model in my database. If the users want to modify the record then I need to associate the value stored in my database with the ViewController. So for example, if he picked up the option 2 back then, I need to make sure the second row in the first section has a different color, and that two rows in the second sections are displayed when he tries to access the view.
Here's some code :
func setNonSelectedCellColor(indexPath: NSIndexPath) {
let currentCell = tableView.cellForRowAtIndexPath(indexPath)
currentCell?.textLabel?.textColor = UIColor.tintColor()
for var nbr = 0; nbr <= 3; nbr++ {
let aCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: nbr, inSection: 0))
let aCellIndexPath = tableView.indexPathForCell(aCell!)
if aCellIndexPath?.row != indexPath.row {
aCell?.textLabel?.textColor = UIColor.blackColor()
}
}
}
func hideAndDisplayPriseCell(numberToDisplay: Int, hideStartIndex: Int) {
for var x = 1; x < numberToDisplay; x++ {
let priseCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: x, inSection: 1))
priseCell?.hidden = false
}
if hideStartIndex != 0 {
for var y = hideStartIndex; y <= 3; y++ {
let yCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: y, inSection: 1))
yCell?.hidden = true
}
}
}
These two functions are getting called every time the user touches a row :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let path = (indexPath.section, indexPath.row)
switch path {
case(0,0):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(1, hideStartIndex: 1)
data["frequencyType"] = Medecine.Frequency.OneTime.rawValue
case(0,1):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(2, hideStartIndex: 2)
data["frequencyType"] = Medecine.Frequency.TwoTime.rawValue
case(0,2):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(3, hideStartIndex: 3)
data["frequencyType"] = Medecine.Frequency.ThreeTime.rawValue
case(0,3):
setNonSelectedCellColor(indexPath)
hideAndDisplayPriseCell(4, hideStartIndex: 0)
data["frequencyType"] = Medecine.Frequency.FourTime.rawValue
default:break
}
}
I store the values in a dictionary so I can tackle validation when he saves.
I'd like the first two functions to be called right after my tableView has finished loading. For example, I can't ask the data source to show/hide 1 or more rows when I initialize the first row because those are not created yet.
As I said this works almost as intended if those functions are called in the viewDidAppear because it doesn't select the row immediately nor does it show the appropriate number of rows in the second sections as soon as possible. I have to wait for 1-2s before it does.
If you have the data already that is used to populate the tableView then can't you use that data itself in the function? I am presuming that the data is in the form of an array of objects which you are displaying in the table view. So you already have access to that data and could use it in the function.
But if that's not the case then and if your table view has only 8 rows then you can try implementing this function and inside that check the indexPath.row == 7 (8th row which is the last one).
tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
Since all your rows are visible in one screen itself without scrolling you could use this function to determine that all the cells have been loaded and then call your function.