Accessing cells that aren't visible on a UITableView - ios

All my cells have a switch that moves the cell to the bottom of the table. It works fine, however when there is enough cells to drop below the visible bounds, I receive the error that the cell doesn't exist. This doesn't work because I need to update ALL cells sender.tag.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: TaskTableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! TaskTableViewCell
var task = tasks[indexPath.row]
let index = tasks.indexOf(task)!
cell.cellSwitch.tag = index
cell.addSubview(cell.cellSwitch)
cell.tag = index
cell.cellSwitch.addTarget(self, action: "switched:", forControlEvents: UIControlEvents.TouchUpInside)
return cell
}
func switched(sender: UIButton) {
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let finalIndexPath = NSIndexPath(forRow: tasks.count - 1, inSection: 0)
let task = tasks[indexPath.row]
tasks.removeAtIndex(indexPath.row)
tasks.append(task)
self.taskTableView.moveRowAtIndexPath(indexPath, toIndexPath: finalIndexPath)
for var i = 0; i < tasks.count; i ++ {
let cellIndex = NSIndexPath(forRow: i, inSection: 0)
let cell: TaskTableViewCell = self.taskTableView.cellForRowAtIndexPath(cellIndex) as! TaskTableViewCell
cell.cellSwitch.tag = cellIndex.row
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cellToBeReturned = UITableViewCell()
if indexPath.row != tableView.numberOfRowsInSection(0) - 1 {
let cell: TaskTableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! TaskTableViewCell
var task = tasks[indexPath.row]
cell.cellFlatSwitch.removeFromSuperview()
cell.currentStreak.removeFromSuperview()
switch(task.icon) {
case "heart"?:
task.color = colors[0]
case "meditate"?:
task.color = colors[1]
case "leaf"?:
task.color = colors[2]
case "run"?:
task.color = colors[3]
case "muscle"?:
task.color = colors[4]
default:
task.color = .blackColor()
}
switch(task.icon) {
case "heart"?:
cell.pretickButton.setImage(Icons.imageOfHeartIcon, forState: .Normal)
case "meditate"?:
cell.pretickButton.setImage(Icons.imageOfMeditationIcon, forState: .Normal)
case "run"?:
cell.pretickButton.setImage(Icons.imageOfRunIcon, forState: .Normal)
case "leaf"?:
cell.pretickButton.setImage(Icons.imageOfLeafIcon, forState: .Normal)
case "muscle"?:
cell.pretickButton.setImage(Icons.imageOfMuscleIcon, forState: .Normal)
default:
cell.pretickButton.setImage(Icons.imageOfLeafIcon, forState: .Normal)
}
if task.longestStreak >= 21 {
cell.starImage.alpha = 1
} else {
cell.starImage.alpha = 0
}
if localSettings.habitPreview == "dayCounter" {
cell.currentStreak = LTMorphingLabel(frame: CGRectMake(11, 29, 39, 31))
cell.currentStreak.morphingEffect = LTMorphingEffect.Anvil
cell.currentStreak.textAlignment = NSTextAlignment.Center
cell.currentStreak.text = String(task.consecutiveDays!)
cell.addSubview(cell.currentStreak)
cell.streakSublabel.textAlignment = .Center
cell.currentStreak.adjustsFontSizeToFitWidth = true
cell.streakSublabel.adjustsFontSizeToFitWidth = true
cell.growthPreviewImage.hidden = true
cell.streakSublabel.hidden = false
} else {
if task.growth > 0 && task.growth <= 21 {
cell.growthPreviewImage.image = UIImage(named: "growth\(task.growth!)")
} else if task.growth <= 0 {
cell.growthPreviewImage.image = UIImage(named: "growth1")
} else if task.growth > 21 {
cell.growthPreviewImage.image = UIImage(named: "growht21")
}
cell.growthPreviewImage.hidden = false
cell.streakSublabel.hidden = true
}
if task.consecutiveDays == 1 {
cell.streakSublabel.text = "day"
} else {
cell.streakSublabel.text = "days"
}
cell.taskLabel.adjustsFontSizeToFitWidth = true
cell.taskLabel.text = task.name
cell.taskLabel.textColor = task.color
cell.cellFlatSwitch = AIFlatSwitch(frame: CGRectMake(300, 15, 70, 70))
cell.cellFlatSwitch.lineWidth = 2.3
cell.cellFlatSwitch.strokeColor = task.color!
cell.cellFlatSwitch.trailStrokeColor = task.color!
let index = tasks.indexOf(task)!
cell.pretickButton.tag = index
cell.backgroundColor = UIColor.clearColor()
cell.addSubview(cell.cellFlatSwitch)
cell.tag = index
cell.bringSubviewToFront(cell.pretickButton)
cell.pretickButton.addTarget(self, action: "switched:", forControlEvents: UIControlEvents.TouchUpInside)
cell.sendSubviewToBack(cell.cellFlatSwitch)
if task.doneToday == true {
cell.cellFlatSwitch.selected = true
cell.pretickButton.alpha = 0.0101
} else {
cell.cellFlatSwitch.setSelected(false, animated: false)
cell.pretickButton.alpha = 1.0
}
cellToBeReturned = cell
} else {
let cell: AddNewTableViewCell = tableView.dequeueReusableCellWithIdentifier("add") as! AddNewTableViewCell
cell.tag = indexPath.row
cellToBeReturned = cell
}
return cellToBeReturned
}
Any tips or help is greatly appreciated.

You are doing it wrong.
Don't call methods with NSIndexPath object references to move cells around. The implementation of UITableView suggests you should use one data source (could be an array, a dictionary, a custom NSObject, NSFetchedResultsController, etc) to implement all the relevant UITableViewDataSource and UITableViewDelegate delegate methods and you make updates to your data source first then call either reloadData or beginUpdates and endUpdates on your UITableView whenever you need to refresh your view.
In your case, I would simply use 2 NSMutableArray's, one for items with, one for without tags. Your switch handler should retrieve the item from the cell index path and move it from one array to the other. Then you can call beginUpdates and move your cell appropriately.
They key is to make changes to your data source first and in a way that is consistent to your UITableViewDataSource methods, especially the cellForAtIndexPath method. Basically the error you get is most likely because your data source is out of sync with the visible cells.
Hope this helps.

Related

UIswitch duplicate issue in table cell

In my App, I have a tableview with 7 sections and each section has 8cells containing Stepper(in UIView),UIButton(in UIView) and UISwitch(in UIview).
Now I am able to set and get values for stepper and UIButton But my issue is with UISwitch.
It is getting reused and shows On/Off states simultaneously(wiered UI).
Also I have maintained an Array to save status of switch but it doesnot reflect properly.
Below is the code
var myData1 : [Bool] = [false,false,false,false,false,false,false,false]
let cell:DefaultTableViewCell = (self.tblviewSwitchPOints.dequeueReusableCell(withIdentifier: "defaultcell", for: indexPath) as? DefaultTableViewCell)!
cell.viewForButton.tag = indexPath.row
cell.selectionStyle = .none
cell.viewForSwitch.tag = indexPath.row
print("in cell for row")
if indexPath.section == 0
{
switch indexPath.row
{
case 0:
cell.lblMain.text = timePerDay[indexPath.row]
cell.lblTemp.text = row1[0]
cell.lblTime.text = row1[1]
cell.lblMain.tag = indexPath.row
cell.viewForStepper.addSubview(valueStepper1)
cell.viewForButton.addSubview(btnCustom1)
cell.viewForSwitch.addSubview(switchOnOff1)
if myData1.count==0
{
}else
{
switchOnOff1.isOn = myData1[0]
}
switchOnOff1.tag = 0
switchOnOff1.addTarget(self,action:#selector(actionOnOff1(sender:)), for:.valueChanged )
let temp = arrTemp1[indexPath.row]
if arrTimeInDouble1.count==0
{
}else if temp.elementsEqual("FFFF")
{
valueStepper1.value = 5
}else
{
valueStepper1.value = arrTimeInDouble1[indexPath.row]
}
valueStepper1.tag = indexPath.row
btnCustom1.tag = indexPath.row
if arrTime1.count==0
{
}else
{
btnCustom1.titleLabel?.text = arrTime1[indexPath.row]
}
valueStepper1.addTarget(self, action: #selector(valueChangedTemp1(sender:)), for: .valueChanged)
btnCustom1.addTarget(self, action:#selector(datePickerTapped1), for: .touchUpInside)
print("in cell fro row1\(myData1)\(switchOnOff3.isOn)\(switchOnOff1.tag)")
return cell
case 1:
cell.lblMain.text = timePerDay[indexPath.row]
cell.lblTemp.text = row2[0]
cell.lblTime.text = row2[1]
cell.lblMain.tag = indexPath.row
cell.viewForStepper.addSubview(valueStepper2)
cell.viewForSwitch.addSubview(switchOnOff2)
if myData1.count==0
{
}else
{
switchOnOff2.isOn = myData1[1]
}
switchOnOff2.tag = indexPath.row
if arrTimeInDouble1.count==0
{
}else
{
valueStepper2.value = arrTimeInDouble1[indexPath.row]
}
cell.viewForButton.addSubview(btnCustom2)
btnCustom2.tag=indexPath.row
valueStepper2.tag = indexPath.row
if arrTime1.count==0
{
}else
{
btnCustom2.titleLabel?.text = arrTime1[indexPath.row]
}
switchOnOff2.addTarget(self, action: #selector(actionOnOff1(sender:)), for:.valueChanged )
valueStepper2.addTarget(self, action: #selector(valueChangedTemp1(sender:)), for: .valueChanged)
btnCustom2.addTarget(self, action:#selector(datePickerTapped1), for: .touchUpInside)
return cell
}
A lot may have gone wrong but what I suspect in your case is that each of your switches has multiple target/action pairs assigned; You dequeue a cell and then call addTarget on switches where a previous target is still on the switch.
In any case what is best done when dealing in these situations is to put most of your code into your table view cell. I actually suggest you to use delegate to get messages back from cell to your view controller so dequeuing should look something like this:
let cell: DefaultTableViewCell = tableView.dequeueReusableCell(withIdentifier: "defaultcell", for: indexPath) as! DefaultTableViewCell
cell.myObject = myObjectSections[indexPath.section][indexPath.row]
cell.delegate = self
return cell
Nothing else. Now cell should do all the logic:
class DefaultTableViewCell: UITableViewCell {
weak var delegate: DefaultTableViewCellDelegate?
var myObject: MyObject? {
didSet { refresh() }
}
override func awakeFromNib() {
super.awakeFromNib()
self.switch.addTarget(self, action:#selector(switchMoved)), for:.valueChanged)
}
func refresh() {
// Change all UI related stuff here
}
#objc private func switchMoved() {
if let myObject = myObject {
delegate?.defaultTableViewCell(self, didChangeWhateverStateOf:myObject to: switch.on)
}
}
}
Naturally a protocol needs to be defined appropriately. For my case that is
protocol DefaultTableViewCellDelegate: class {
func defaultTableViewCell(_ sender: DefaultTableViewCell, didChangeWhateverStateOf myObject: MyObject, to flag: Bool)
}
and your view controller needs to implement this protocol.
Sometimes it may actually make sense to add an index path with the data as well. To do so you simply upgrade this code so cell also contains var indexPath: IndexPath!. Now after dequeuing a cell you assign its new index path. And then in your delegate method you can access it at any time:
extension MyViewController: DefaultTableViewCellDelegate {
func defaultTableViewCell(_ sender: DefaultTableViewCell, didChangeWhateverStateOf myObject: MyObject, to flag: Bool) {
print("This is cell at section \(sender.indexPath.section), row: \(sender.indexPath.row)")
}
}
Would be good if you create a custom cell with UISwitch in it. Then you can assign value to the switch like this -
var cell : SwitchTableViewCell? = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") as? SwitchTableViewCell
if(cell == nil)
{
cell = SwitchTableViewCell(style: .default, reuseIdentifier: "CellIdentifier")
}
if(indexPath.section == 0)
{
if(myData1.count != 0)
{
cell?.switchOnOff.isOn = myData1[indexPath.row];
}
}
return cell!;

How to change the view and at the same time still let the user edit in the TextField?

So I'm having an array of phone numbers and I iterate through it, if one from the array is the same as the one in the textField, I will reload the table row and show some image with number ok and stuff like that. Else let's say if the user now changes that number which was good, like deletes a decimal, now the number no longer matching the ones from the array so I need to change that cell view.But this current code reloading my table for every change in the textField and by reloading it won't stay anymore in editing mode.
#objc func textFieldValueCahnged(_ textField: UITextField) {
if textField.tag == 0 {
if !shouldValidatePhone {
for phone in self.phoneNumbers {
if phone == textField.text! {
self.phoneNumber = phone
GlobalMainQueue.async {
self.phoneValidated = true
// self.reloadCellForIdentifier("ExtraFieldCell")
self.mainTableView.reloadRows(at: [self.rowindexpath!], with: .fade)
}
break
} else {
GlobalMainQueue.async {
self.phoneValidated = false
self.phoneNumber = textField.text!
self.mainTableView.reloadRows(at: [self.rowindexpath!], with: .fade)
// TODO: IF IT 'S typed a different number that the ones in self.phoneNumbers array we need to update the view with the cell.elementsAreHidden()
}
break
}
}
}
} else {
verifyCode = textField.text!
}
}
the tableView cell:
let cell = tableView.dequeueReusableCell(withIdentifier: "PhoneValidationCell") as! PhoneValidationCell
self.rowindexpath = indexPath
cell.label.text = "\(currentField.label):"
cell.textField.addTarget(self, action: #selector(textFieldValueCahnged(_:)), for: .editingChanged)
cell.sendCodeBtn.addTarget(self, action: #selector(sendCodeButtonPressed(_:)), for: .touchUpInside)
cell.textField.tag = 0
cell.sendCodeBtn.tag = 0
cell.textFieldForVeryfing.addTarget(self, action: #selector(textFieldValueCahnged(_:)), for: .editingChanged)
cell.verifyCodeBtn.addTarget(self, action: #selector(sendCodeButtonPressed(_:)), for: .touchUpInside)
cell.textFieldForVeryfing.tag = 1
cell.verifyCodeBtn.tag = 1
if self.phoneNumbers.isEmpty {
if let phoneNumbers = currentField.userPhones {
self.phoneNumbers = phoneNumbers
}
}
if !phoneValidated && !shouldValidatePhone {
if let value = self.extraFieldsValues[currentField.name] as? String {
for i in phoneNumbers {
if i == value {
cell.phoneVerified()
break
} else {
cell.elementsAreHidden()
}
}
if phoneNumbers.isEmpty {
cell.elementsAreHidden()
}
cell.label.textColor = UIColor.black
cell.textField.text = value
self.phoneNumber = value
} else {
cell.label.textColor = Theme.placeholderColor()
cell.textField.text = ""
}
} else {
// cell.phoneVerified() /// check here
cell.textField.text = self.phoneNumber
}
if shouldValidatePhone && !phoneValidated {
cell.phoneToBeVerified()
}
if phoneValidated && shouldValidatePhone {
cell.phoneVerified()
}
basically when phoe number is in the textField I'm showing cell.elementsAreHidden() . when number needs to be validated cell.phoneToBeVerified() . and when it s verified cell.phoneVerified()
You should store the indexPath of the cell you are editing and focus again on it's textfield after the reload by calling cell.textField.becomeFirstResponder().
For example:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == self.rowindexpath {
(cell as? ProjectTableViewCell)?.textField.becomeFirstResponder()
}
}

how to make previous values to set in NHRangeSlider?

I had used NHRangeSlider for setting price range in my application placed in table view in which after setting the min and max ranges when i move to another view controller i was getting error and the last set values and normal values has been set in the slider how to clear this error ?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
let cell = tableView.dequeueReusableCell(withIdentifier: "sliderCell", for: indexPath) as! SliderCell
tableView.isHidden = false
activityIndicator.stopAnimating()
activityIndicator.hidesWhenStopped = true
let sliderView = NHRangeSliderView(frame: CGRect(x: 16, y: 28, width: self.view.bounds.width - 32, height: 80))
sliderView.sizeToFit()
cell.contentView.addSubview(sliderView)
return cell
}
else{
let cell = tableView.dequeueReusableCell(withIdentifier: "brandCell", for: indexPath) as! BrandCell
if selected == true{
let value = values.count
if (value == 1) {
cell.brandsLabel.text = values[indexPath.row]
}
else if (value == 0) {
let string = "no brand selected"
cell.brandsLabel.text = string
}
else {
let total = " &"+" \(value-1) more"
print(total)
cell.brandsLabel.text = "\(values[0])" + total
}
}
return cell
}
}
image is as shown here
It looks like you are always adding a new instance of NHRangeSliderView to your cell:
let cell = tableView.dequeueReusableCell(withIdentifier: "sliderCell", for: indexPath) as! SliderCell
tableView.isHidden = false
activityIndicator.stopAnimating()
activityIndicator.hidesWhenStopped = true
let sliderView = NHRangeSliderView(frame: CGRect(x: 16, y: 28, width: self.view.bounds.width - 32, height: 80))
sliderView.sizeToFit()
cell.contentView.addSubview(sliderView)
return cell
Assuming that you are reloading the cell/tableview somewhere else you will find that this piece of code is always adding a brand new slider view to the content view of your cell, since it is reusing the cell that already has an instance of the NHRangeSliderView in it.
In this situation I would recommend creating a custom table view cell that already has the slider view attached this way you will have a slightly simpler cellForRow(atIndexPath:) method, but also you won't need to worry about continually adding the view.

TableView gets inconsistent behaviour when scrolling down/up

I have a ViewController that has a TableView. The tableView cell has a StackView with two images in it and outside it a label.
In the tableView:cellForRowAtIndexPath I get the cell and my data is inside a arrayList. And this is what I do:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "NamesTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NamesTableViewCell
let _name = _Names[indexPath.row]
if indexPath.row % 2 != 0 {
cell.backgroundColor = UIColor(hexString: "ededed")
}
cell.labelName.text = _name.name
if _name.gender! == "M" {
cell.pinkCircleImageView.hidden = true
} else if _name.gender! == "F" {
cell.blueCircleImageView.hidden = true
}
return cell
}
So as you can see I hide the images depending on the gender of the name and also change the background of every other cell.
Now, the behavior I am seeing is:
https://gyazo.com/1b2d39696892b7fb2f15b71696d9a925
The gender is available for every object, I've checked.
What do you guys think? Thanks!
cell.pinkCircleImageView.hidden = false
cell.blueCircleImageView.hidden = false
if _name.gender! == "M" {
cell.pinkCircleImageView.hidden = true
cell.bringSubviewToFront(blueCircleImageView)
} else if _name.gender! == "F" {
cell.blueCircleImageView.hidden = true
cell.bringSubviewToFront(pinkCircleImageView)
}
if indexPath.row % 2 == 0
{
cell.backgroundColor=UIColor.whiteColor()
}
else
{
cell.backgroundColor=UIColor(red: 248/255, green: 248/255, blue: 248/255, alpha: 1.0)
}

Swift: Table Cells not setting up buttons correctly

I have been following a tutorial online for a yikyak clone using swift and parse. i am storing the objectIDs of the upvoted/downvoted items using coreData. When the tableview cell is loaded, it checks if the objectID on parse is in coreData and responds accordingly by adding a background image to the specific button and disabling both the up and down vote buttons. However, I am facing an issue where scrolling up and down a few times causes random cells to have the background and have their buttons disabled.
Here is a link to the code (cellForRowAtIndexPath:):
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as PullTableViewCell
let restaurant = self.restaurantData[indexPath.row]
cell.scoreLabel.text = "\(score)"
cell.plusButton.tag = indexPath.row;
cell.plusButton.addTarget(self, action: "plusOne:", forControlEvents: .TouchUpInside)
cell.minusButton.tag = indexPath.row
cell.minusButton.addTarget(self, action: "minusOne:", forControlEvents: .TouchUpInside)
if (cell.plusButton.enabled) {
// upvotes
let request = NSFetchRequest(entityName: "Upvotes")
let moc:NSManagedObjectContext = self.managedObjectContext!
var error: NSErrorPointer = nil
self.upvoteData = moc.executeFetchRequest(request, error: error) as [Upvotes]
for (var i = 0; i < self.upvoteData.count; i++) {
if (self.upvoteData[i].objectid == self.objectIDs[indexPath.row]) {
NSLog("the cell is in view is \(indexPath.row)")
cell.plusButton.backgroundColor = UIColor.blueColor()
cell.minusButton.enabled = false
cell.plusButton.enabled = false
}
}
// downvotes
let request2 = NSFetchRequest(entityName: "Downvotes")
let moc2:NSManagedObjectContext = self.managedObjectContext!
var error2: NSErrorPointer = nil
self.downvoteData = moc2.executeFetchRequest(request2, error: error2) as [Downvotes]
for (var i = 0; i < self.downvoteData.count; i++) {
if (self.downvoteData[i].objectid == self.objectIDs[indexPath.row]) {
cell.minusButton.backgroundColor = UIColor.blueColor()
cell.minusButton.enabled = false
cell.plusButton.enabled = false
}
}
}
return cell
}
Does it have to do with asynchronous processing?
The tableview actually re-creates cells on the fly as you scroll up and down. So when a cell is scrolled out of view it can get destroyed and re-created.
You need to store cell properties inside of a map and then re-initialize the cell each time.
Here is an example from my own code:
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("selectPhotoCell", forIndexPath: indexPath) as B_SelectPhotoControllerViewCell
cell.indexPath = indexPath
let asset = currentAssetAtIndex(indexPath.item)
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize:_cellSize, contentMode: .AspectFit, options: nil)
{
result, info in
if(cell.indexPath == indexPath)
{
cell.imageView.image = result
cell.imageView.frame = CGRectMake(0, 0,self._cellSize.width,self._cellSize.height)
for editImage in self._editImageChicklets
{
if(editImage.selectedPhotoIndexPath != nil && editImage.selectedPhotoIndexPath == indexPath)
{
cell.number = editImage.selectedPhotoDisplayNumber!
}
}
}
}
return cell
}
This is an example of a UICollectionView that uses photo images from the users photo roll. I keep track of the indexPath, and if its a match, then I assign the images to the property.
In your case you need to do the same. Keep a map of the cell properties and the index path:
Dictionary<NSIndexPath,CustomCellPropertiesObject>
Then go through and re-initialize it appropriately.

Resources