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.
Related
I have a tableview that the user edits information in using textfields, and I store that information into an array that keeps track of all the values. The issue occurs when the user scrolls back to a cell they already edited and the values the added previously are now values from other cells.
I understand that cells are reused, and as a result their data needs to be updated whenever they are being viewed again. I also learned that cellforrowat is called every time a cell is loaded into the view as opposed to just the first time a cell is created. I made a test project to figure out my problem.
My first attempt at solving the problem went like so
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
if cellInformation.count < (indexPath.row + 1) // Newly made cell
{
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row) // Cell we've seen before
{
cell.configure(Value: cellInformation[indexPath.row]!.value) // Sets the textField.text to be the same as the cells value
}
This worked better but when I scrolled back to the top of my tableview, the top most cells were still getting random data. My next attempt generated an ID tag for each cell, and then checking if the id tag of the cell at cellforrowat matched any of the one's in the array.
if cellInformation.count < (indexPath.row + 1) // 0 < 1
{
cell.idTag = idTagCounter
idTagCounter += 1
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row)
{
for i in 0...idTagCounter - 1
{
if(cell.idTag == cellInformation[i]?.idTag)
{
cell.configure(Value: cellInformation[i]!.value)
}
}
cell.configure(Value: cellInformation[indexPath.row]!.value)
}
This got pretty much the same results as before. When I debugged my program, I realized that when i scroll down my tableview for the first time, indexPath.row jumps from a value like 7 down to 2 and as I scroll more and more, the row goes further away from what it should be for that cell until it eventually stops at 0 even if there are more cells i can scroll to. Once the row hits 0, cellforrowat stops being called.
Any ideas on how i can accurately assign a cells values to the information in my array?
Your premise is wrong:
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
You should set up a model object that contains the data for the entries in your table view, and your cellForRowAt() method should fetch the entry for the requested IndexPath.
Your model can be as simple as an array of structs, with one struct for each entry in your table. If you use a sectioned table view you might want an array of arrays (with the outer array containing sections, and the inner arrays containing the entries for each section.)
You should not be building your model (array) in calls to cellForRowAt().
You also should not, not NOT be storing cells into your model. You should store the data that you display in your cells (text strings, images, etc. Whatever is appropriate for your table view.)
Assume that cellForRowAt() can request cells in any order, and ask for the same cells more than once.
Say we want to display an array of animals, and a numeric age:
struct Animal {
let species: String
let age: Int
}
//Create an array to hold our model data, and populate it with sample data
var animals: [Animal] = [
Animal(species: "Lion", age: 3),
Animal(species: "Tiger", age: 7),
Animal(species: "Bear", age: 4)
]
//...
func cellForRow(at indexPath: IndexPath) -> UITableViewCell? {
let cell = dequeueReusableCell(withIdentifier: "cell" for: indexPath)
let thisAnimal = animals[indexPath.row]
cell.textLabel.text = "\(thisAnimal.species). Age = \(thisAnimal.species)"
}
Note that for modern code (iOS >=14), you should really be using UIListContentConfigurations to configure and build your cells.
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.
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.
Solved:
Phillip Mills' solution has solved the issue presented in this post, as outlined below.
Currently, when a user searches for a beer in the application, the beer shows up in a UITableViewCell subclass, which displays the name of the beer and the brewery name in UILabel instances. The cell also contains four buttons below the labels: likeButton, tryButton, dislikeButton, and deleteButton.
When a user searches the API database for a beer, the user can then save a beer to a specific category in Core Data by using one of the buttons in the cell. These saved beers will then show up in a UITableView elsewhere in the app. I am able to successfully save and delete beers from the cells, and they do show up in the correct category's UITableView instance. However, if a returned beer result is not saved in Core Data, I want the deleteButton to not be shown in the cell that is populated from the JSON of the API because the app is set up for the user to save a beer, change a beer's category, or delete a beer from the search results UITableView instance.
I have the saving, changing of a beer's category, and deleting of a beer working correctly in both the category UITableView instances and in the search results UITableView. My issue arises when displaying the buttons in the results UITableView.
When results are returned from he API, I only want the deleteButton to be displayed if there is a beer saved in Core Data that matches the returned result. I'm guessing that I need to perform this comparison in cellForRowAtIndexPath:, but I feel like I am going about it incorrectly because I can get the deleteButton to either be visible or be hidden in all cells, regardless of whether the beer is saved in Core Data or not.
Here is my current implementation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if scopeButtonIndex == 0 {
let beerCell = tableView.dequeueReusableCellWithIdentifier("foundBeerResultCell", forIndexPath: indexPath) as! BeerTableViewCell
let beer = foundBeerResults[indexPath.row]
beerCell.beerNameLabel.text = beer.name
beerCell.breweryNameLabel.text = beer.brewery
beerCell.delegate = self
for savedBeer in savedBeers {
if beer.name == savedBeer.beerName && beer.brewery == savedBeer.beerBrewery {
beerCell.deleteButton.hidden = false
} else if beer.name != savedBeer.beerName && beer.brewery != savedBeer.beerBrewery {
beerCell.deleteButton.hidden = true
}
}
return beerCell
}
}
Currently savedBeers in an array of the saved beers in Core Data. As you can see, I am taking each beer that is returned from the API and saved in foundBeerResults, which populates the results UITableView instance. I am then looping through savedBeers to see if each returned beer in foundBeerResults matches the saved beer. If the information matches, I want the deleteButton to be visible so that the user can delete the saved beer directly from the search results. If a returned beer result does not match a currently saved beer, I want the deleteButton to be invisible.
Am I incorrect by assuming that I should not be iterating through arrays in cellForRowAtIndexPath:? It seems inefficient. However, I am not sure of how to solve this problem.
Looping might or might not be a performance problem. You can measure for that but let's assume "not" for the moment since it seems like that would be a fairly small array you're using.
I think your problem is that you're not stopping the loop once you get a right answer.
How about:
beerCell.deleteButton.hidden = true
for savedBeer in savedBeers {
if beer.name == savedBeer.beerName && beer.brewery == savedBeer.beerBrewery {
beerCell.deleteButton.hidden = false
break
}
}
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.