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
}
}
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 have tableview where I need to show few sections. You should imagine this table like a playlist of your songs. In the top first section I need to display a cell with button which will add more songs to the playlist and other sections of tableview are header titles of Music category (like pop, rock and etc). Each of these sections contains cells which are songs names.
I have an array of songs called like this: var songsGroups = [SongGroup]. Which is actually my datasource.
SongGroup contains few properties:
var categoryName: String
var songs: [Songs]
But the problem appears on the next level. I every time need to check indexPath.section and do like this:
if indexPath.section == 0 {
// this is a section for ADD NEW SONG BUTTON cell no need in header title as there is no data repression only static text on the cell.
} else {
var musicCategoryName = songsGroups[indexPath.seciton - 1]. categoryName
headerTitle.title = musicCategoryName
}
As you see my code became magical by adding this cool -1 magical number. Which I replay don't love at all.
As an idea for sure I can try to combine my ADD NEW SONG BUTTON section (by adding some additional object) with songsGroups array and create NSArray for this purposes. Like in Objective-C as you remember. So then my datasource array will looks like this:
some NSArray = ["empty data for first cell", songsGroups[0], songsGroups[1]... etc]
So then there is no need to check any sections we can trust our array to build everything and even if I will add more empty data cells there is no need for me to handle my code via if block and adding tons of magical numbers.
But the issue I see here that we don't use explicit types of array and it's upset.
So maybe you know more beautiful solutions how to resolve my issue.
You can introduce a helper enum:
enum Section {
case empty
case songCategory(categoryName: String, songs: [String])
}
Your data source would then look something like this:
let datasource: [Section] = [.empty, .songCategory(categoryName: "Category1", songs: ["Song 1", "Song2"])]
So now you can use pattern matching to fill the table view:
let section = datasource[indexPath.section]
if case let .songCategory(categoryName, songs) = section {
headerTitle.title = categoryName
} else {
// this is a section for ADD NEW SONG BUTTON cell no need in header title as there is no data repression only static text on the cell.
}
I am not sure if I understand you right. But is seems to me that you want to display
1) something that lets the user add a new song by tapping a button, and
2) a table of songs, sectioned into groups.
If this is the case, why don’t you put the add new song button in the table header view, and all your song groups and songs in a 2-dim array used as your dataSource?
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 am trying to use a UITableview with multiple selection on and a check mark accessory view for selected rows. This is mostly working if I turn on and off the accessory view in tableView:didSelectRow.
However, I tried to build a selectAll method, and I found that the array of selected cells was being cleared after I had spun through all the cells and selected them if I then call reloadData().
I suspect reloading the table clears selection. I don't know of any other way to have all the cells drawn after I set the selected flag and accessory view.
I am wondering if I need to keep my own array of selected rows. Has anyone else built something like this? Its seems like a common scenario.
Any tips or sample code appreciated.
Take an Array and add the indexPath of each selected cell into it and put a condition in cellForRowAt... that if the Array contains that particular indexPath, set it as selected.
There are two approaches you can take. One is to track the selected row numbers. To do this, you can use an NSMutableIndexSet or its Swift counterpart IndexSet.
Essentially, when a row is selected, add it to the set. When you deselect it, remove it from the set. In cellForRowAtIndexPath you can use containsIndex to determine if a check mark should be shown or not.
Since you explicitly mention an issue with selection when you reload the table, it is worth considering the issue with storing row numbers (whether in a set or an array), and that is that row numbers can change.
Say I have selected rows 4,7 and 9 and these values are stored in the index set. When I reload the data, new data may have been inserted after the "old" row 8, so now I should be selecting rows 4,7 and 10, but I will be selecting 4,7 and 9 still.
A solution to this is to store some sort of unique identifier for the data that should be selected. This will depend on your data, but say you have a string that is unique for each item. You can store this string in a NSMutableSet or Swift Set, which again makes it easy to check if a given item is selected using contains
you need add some functionality in cellForRowAtIndexPath method like this ang your view controller code like this
let we take one example of photo gallery application
class CreateEvent: UIViewController,UITableViewDataSource,UITableViewDelegate {
var yourArray : [Photo] = [Photo]()
//MARK: - Content TableView Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
let objPhoto = yourArray[indexPath.row]
if objPhoto.isPhotoSelected == true
{
cell.accessoryType = .Checkmark
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let objPhoto = yourArray[indexPath.row]
objPhoto.isPhotoSelected = true
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell.accessoryType = .Checkmark
}
//MARK: Action Method
func selectAllPhoto()
{
for objPhoto in yourArray
{
objPhoto.isPhotoSelected = true
}
tableView.reloadData()
}
}
and one more thing you need to create your custom object like
class Photo: NSObject {
var photoName:String = ""
var isPhotoSelected = false
}
hope this will help you
The best approach for multiple selection is
Take a model object, in that take all your attributes and one extra boolean attribute (like isSelected) to hold the selection.
In case of selecting a row
Fetch the relevant object from the array
Then update the isSelected boolean (like isSelected = !isSelected) and reload table.
In case of select all case
Just loop through the array.
Fetch the model object from array.
make the isSelected = true.
After completion of loop, reload the table.
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.