I have a simple table view. I want the first and last rows to have a special picture showing the beginning and end of a line. However, I am getting the wrong identification. The table view thinks that cells in the middle are the ends cells. I believe that IOS is caching the rows and and loading them when there needed as when I scroll up and down some of the arrows change picture(they should be static once loaded). Also the documentation says here Any idea how to fix this and identify the last and first row regardless of caching? (Btw locations is the arrow used to load the cells and the first and last positions of the array are where the special picture should be).
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var stop = locations[indexPath.row]
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CustomRouteViewCell
cell.locationTitle.text = "\(stop)"
cell.downTime.text = BusRoute.getRecentDepartures(stop, direction: direction, name: name)
println(indexPath.row)
cell.indexRow = indexPath.row
if(cell.indexRow == 0)
{
cell.downImage.image = UIImage(named: "beginningRoute.png")
}
else if(cell.indexRow == locations.count - 1)
{
cell.downImage.image = UIImage(named: "endRoute.png")
}
return cell
}
The problem may not be you're misidentifying the first and last cells (that looks fine so long as you've only got one section in your UITableView). The problem could be reusing cells: you're not setting the cell's downImage.image to nil and the cell may previously have been the first or last cell.
Try the following:
switch indexPath.row {
case 0:
cell.downImage.image = UIImage(named: "beginningRoute.png")
case self.tableView(tableView, numberOfRowsInSection: 0) - 1:
cell.downImage.image = UIImage(named: "endRoute.png")
default:
cell.downImage.image = nil
}
Related
I have a working switch and a switch state that keeps track on which switch is flipped on using the Indexpath's row number and bool stored in a dictionary. While that works excellent for one section only. I am having a difficult time prevent it from spilling over to the next section as seen here:
Section 0 Row 3 switch turned on
Section 1 Row 3 switch turned on without me pressing it.
Is there a way to only keep switches on for that particular section? Right now I'm using two prototype cells one for the data I'm displaying which includes only one switch and the other cell for displaying the section header.
Here is some of the code I guess would be helpful in seeing what I put down:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("advCell", forIndexPath: indexPath) as! advDataCell
cell.advDelegate = self
switch(indexPath.section) {
case 0:
cell.lblCategoryItem.text = foodCategories[indexPath.row]["name"]
case 1:
cell.lblCategoryItem.text = activitiesCategories[indexPath.row]["name"]
default:
return cell
}
if advSwitchStates[indexPath.row] != nil {
cell.advOnOffSwitch.on = advSwitchStates[indexPath.row]!
}
else {
cell.advOnOffSwitch.on = false
}
cell.advOnOffSwitch.on = advSwitchStates[indexPath.row] ?? false
return cell
}
func switchCell(advSwitchCell: advDataCell,didChangeValue value: Bool) {
let indexPath = tableView.indexPathForCell(advSwitchCell)!
print("This advanced filter controller has received the switch event.")
advSwitchStates[indexPath.row] = value
}
And what I am using to store the switch states:
var advSwitchStates = [Int: Bool]()
In your cellForRowAtIndexPath you dequeue a recycled cell, like you are supposed to do. You need to fully configure every view in the cell, IN ALL CASES. That means that in ALL cases you need to set a value for your advOnOffSwitch.
In your cellForRowAtIndexPath method you have a switch statement for section values of 0, 1, or any other value. If the section value is not 0 or 1, you return without setting the state of the advOnOffSwitch. If you recycle a cell that had advOnOffSwitch set on, it will stay on, which you don't want. Change your switch statement like this:
switch(indexPath.section) {
case 0:
cell.lblCategoryItem.text = foodCategories[indexPath.row]["name"]
case 1:
cell.lblCategoryItem.text = activitiesCategories[indexPath.row]["name"]
default:
cell.advOnOffSwitch.on = false
return cell
}
With that code, you force the switch to the off position to start for sections other than section 0 or 1.
There are two different issues, first get rid of the early return when setting the name of the cell:
switch(indexPath.section) {
case 0:
cell.lblCategoryItem.text = foodCategories[indexPath.row]["name"]
default:
cell.lblCategoryItem.text = activitiesCategories[indexPath.row]["name"]
}
Secondly you need to track the toggle state with both the section and row as keys since you have multiple sections. The easiest way to do this is probably to make the key of advSwitchStates to be an NSIndexPath.
e.g. declare it as:
var advSwitchStates = [NSIndexPath: Bool]()
and then in cellForRowAtIndexPath
cell.advOnOffSwitch.on = advSwitchStates[indexPath] ?? false
I apologize for the poorly worded question but I simply cannot think of why/how to describe this bug in a short manner.
Basically I have a table view controller displaying two sections, each with a respective custom UITableViewCell subclass. They are displayed perfectly, with the correct model data.
The problem arises when I hit the "done" button, my program traverses through all the cells, gathering the data from each cell and updating the model. The first section goes off without a hitch... but the second section is where trouble hides. The first cell of this section can be casted into the proper subclass, yet any other additional cells don't pass this conditional test and their reuse identifier is nil. I tried checking the cells when they're dequeued but they are being created/reused properly. My code is below and any suggestions will aid my sanity.
Here is the part of my helper function that traverses the cells:
for i in 0..<tableView.numberOfSections {
for j in 0...tableView.numberOfRowsInSection(i) {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: j, inSection: i))
// This section seems to work fine
if(i == 0){
if let gradeCell: PercentTableViewCell = cell as? PercentTableViewCell {
print("retrieveCellInfo: gradeCell - \(gradeCell.getPercent())")
newPercentages.append(gradeCell.getPercent())
}
} else if (i == 1){
print("Else if i == 1 : j == \(j) : \(cell?.reuseIdentifier!)") // Prints "category" for the first cell and "nil" for any other
// This is the cast conditional that only lets one cell through
if let catCell = cell as? EditTableViewCell{
newCategories.append(Category(name: catCell.category!, weight: catCell.weight!, earned: catCell.earned!, total: catCell.total!))
}
}
}
}
Here is my delegate function which works perfectly as far as I can tell. As I said, the UI is displayed correctly:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.section == 0){
/* Grade Parameter Cells */
let cell: PercentTableViewCell = tableView.dequeueReusableCellWithIdentifier("gradeParam", forIndexPath: indexPath) as! PercentTableViewCell
var gradePercentages = course?.getGradePercentages()
// Make sure percentages array is sorted correctly
gradePercentages?.sortInPlace() {
return $0 > $1
}
// Update cell's member variables
cell.initialize(letters[indexPath.row], percent: gradePercentages![indexPath.row])
return cell
} else {
/* Category Cells */
let catcell: EditTableViewCell = tableView.dequeueReusableCellWithIdentifier("category", forIndexPath: indexPath) as! EditTableViewCell
let category = categories![indexPath.row]
catcell.setLabels(category.name, earned: category.earned, total: category.total, weight: category.weight)
return catcell
}
}
The problem arises when I hit the "done" button, my program traverses
through all the cells, gathering the data from each cell and updating
the model
This is your problem. Your cells should reflect your data model, they cannot be relied upon to hold data as once the cell is offscreen it may be re-used for a row that is onscreen.
If data is updated by the user interacting with the cells then you should update your data model as they do that. You can use a temporary store if you don't want to commit changes immediately.
I have a UIViewController, and when a user clicks a button, I want to show a UITableView. I receive the data from the server.
The problem is that there is something weird happening. Sometimes, not all the cells are updated. In other words, in my prototype cell, there are two buttons, with default text, but when I load the data from server not all the buttons text are updated, instead when I scroll the table, they become updated. Also when I **scroll* the table, sometimes the buttons go back to the default text.
Here are my code: (really easy)
class CusinePreferencesTableView: NSObject, UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
row = row + 1
if row < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![row]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
var count = cusines.count/2
count = count + 1
return count
}
}else {
return 0
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
}
And this is the table view (in case you want it)
If you see that you didn't get the description of my problem, I can make a video for you
Update 1
I already call the reloadData as you see here (where I get the data from the server)
func loadCusiens(){
let url = NSURL(string: ConstantData.getWebserviceFullAddress()+"preferences/cusines")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error ) in
if let error = error {
print("error = \(error)")
}
if let data = data {
do{
let jsonArray = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSArray
var results = [String]()
for oneJSON in jsonArray {
let name = oneJSON["name"] as! String
results.append(name)
}
dispatch_async(dispatch_get_main_queue(), {
Preferences2ViewController.cusines = results
self.cusineTableView.reloadData()
})
} catch{
print("This exception happened = \(error)")
}
}
})
task.resume()
}
Update
Now in the else part, i set the text of the right button to "sometext"
this is a video about the problem
http://www.mediafire.com/watch/k2113ovebdvj46d/IMG_0911.MOV
Update
The main problem is not cell reuse, nor the layout problem I explain below, but the use of:
cell.leftButton.titleLabel?.text = oneCusineDataLeft
to set the button text. You must use:
cell.leftButton.setTitle(oneCusineDataLeft, forState: .Normal)
instead. (In essence, a UIButton keeps track of the title text it should display when it is in different "states" - normal, selected, etc. Although the your code changes the text that is currently displayed, as soon as the button changes state, it sets the text back to the stored value. I assume the button state is reset when the cells are displayed. The setTitle method updates the stored value).
Original
Setting aside the cell reuse problem, I don't think your code will achieve quite what you want. As currently coded, the layout would be something like this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 1, right: cuisine 2
Row 2: left: cuisine 2, right: cuisine 3
I'm guessing you actually want this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 2, right: cuisine 3
Row 2: left: cuisine 4, right: cuisine 5
If that's the case, amend your code as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![2*row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
if (2*row+1) < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![2*row+1]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
cell.rightButton.titleLabel?.text = ""
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
return (cusines.count+1)/2
}
}else {
return 0
}
}
I should also note that you would probably be better off using a UICollectionView rather than a UITableView, since the former will provide the multiple column flow much more easily and intuitively.
Looks like a reuse problem. Basically if you follow the examples in Apple and other places, it uses a dequeue function. What this means is that iOS will look for an already existing cell that is no longer being shown and give you that to draw the contents of a new cell. But of course if the old cell had a bunch of stuff set, you will see the old stuff. The solution is to always assign everything to the correct new value.
This can get you easily if your cells have variable information, for instance if there's a picture you set if a certain entry has it, but isn't set if it doesn't have it. What will happen then is when you get an entry with no picture your code might not set it the image to nil, and you'll see the picture of an old cell that was scrolled off.
Please let me explain the reuse of UITableViewCells works
Assume you have a very huge cells and you can see only 3 cells on you screen.
Assume now you see on your screen the following cells:
A
B
C
On the screen... and your datasource is: A B C C B A.
Now you are scrolling down for 1 cell exactly:
So the cell A is going from the Up (it was first cell) down and becoming your last visible cell
So now on the screen you will see
B
C
A // While you want to draw a cell `C` here according to the dataSource
What happens now in cellForRowAtIndexPath...
You are getting the same Cell A from the beginning.
Assume your cell A had only 1 button and a Cell C need to show 2 buttons
Because you have already draw the cell A you draw it with 1 button (lets assume you set the right button hidden property to YES).
So now you want to draw your cell C on the reused cell A
(Assume you set TAGS for the cell types to know what type of cell is popped up to you in the cellForRowAtIndexPath)
And now you can query the tag of the cell
if(myCell.tag == `A`/*(Assume you have an Enum for tags)*/)
{
//So here you will want to Unhide the Right button of the cell (which is currently still cell A
//After you have done with all needed adjustments you will want to set the appropriate tag for this cell to be ready for the next reuse -> so set it's tag to C
//Now next time this cell will get reused it will be drawn as cell `C` and it will think it is a cell `C`
}
I have a table view like this:
when the user tap one row, I want uncheck the last row and check the selected row. So I wrote my code like this:
(for example my lastselected = 0)
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var lastIndexPath:NSIndexPath = NSIndexPath(forRow: lastSelected, inSection: 0)
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastIndexPath) as! TableViewCell
var cell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
cell.checkImg.image = UIImage(named: "check")
lastSelected = indexPath.row
}
every thing working fine when I tap a row without scrolling. I realize that when I run the code and scrolling the table immediately and selected the one row. My program will crash with error:
"fatal error: unexpectedly found nil while unwrapping an Optional value"
the error show in this line:
I don't know what wrong in here?
Because you are using reusable cells when you try to select a cell that is not in the screen anymore the app will crash as the cell is no long exist in memory, try this:
if let lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastIndexPath) as! TableViewCell{
lastCell.checkImg.image = UIImage(named: "uncheck")
}
//update the data set from the table view with the change in the icon
//for the old and the new cell
This code will update the check box if the cell is currently in the screen. If it is not currently on the screen when you get the cell to reused (dequeuereusablecellwithidentifier) you should set it properly before display. To do so you will need to update the data set of the table view to contain the change.
Better approach will be storing whole indexPath. not only the row. Try once i think this will work. I had the same problem in one of my app.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastSelectedIndexPath) as! TableViewCell
var cell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
cell.checkImg.image = UIImage(named: "check")
lastSelectedIndexPath = indexPath
}
EDIT: or you can try this.
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
}
I would like to hide some elements in a custom cell when we overpass a specific number of row. I added more row than the ones visible, because I needed to scroll until the last row without the bouncing effect. But now I have more cells, and I don't need the cells after row > 13.
I tried to setNeedsDisplay the cell with a if else, but the dequeue... method has a bad effect on the cells, when I scroll up, back to the previous cells, they don't have the texts anymore, like the row > 13. Is there a way to use the dequeue method, and let the content for the rows < 13, and remove the content for the rows > 13 ?
Here is some code :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var identifier = ""
if tableView == self.tableView{
identifier = "MyCell"
let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as MyCell
if indexPath.row < 14 {
cell.showContent = true
cell.label.text = "test\(indexPath.row)"
}
else {
cell.showContent = false
cell.label.text = ""
cell.addItem.text = ""
}
cell.setNeedsDisplay()
return cell
}
//MyCell
override func drawRect(rect: CGRect) {
if !showContent {
label.text = ""
addItem.text = ""
}
else {
let path = UIBezierPath()//custom separator that should not be drawn for row > 13
Thanks
You shouldn't modify the text this way in drawRect. You already modified the labels in cellForRow. That's all you need.
That said, this isn't how I would do it. I'd probably create a different cell with its own identifier for empty cells. That way they can be really simple and you don't have to do things like cell.setNeedsDisplay() to get rid of the separator line. So in cellForRow, just return one kind of cell for data rows, and a different kind of cell for empty rows. There's no rule that says all the cells have to be the same class.