Collection view cells duplicate when scrolling - ios

I've got a collection view and I've got a custom class for the cells. Each cell contains a text view, here is the code:
class CustomWriterPageCell: UICollectionViewCell, UITextViewDelegate {
fileprivate let textViewOne: UITextView = {
let tv = UITextView()
tv.backgroundColor = .cyan
tv.text = "Chapter Title"
tv.font = UIFont(name: "Avenir-Roman", size: 27)
tv.textColor = .gray
return tv
}()
}
Here is the cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "WriterPageCellID", for: indexPath) as! CustomWriterPageCell
cell.backgroundColor = .clear
return cell
}
The text view has a placeholder that I've achieved through the following code (This is inside the custom cell class):
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == .gray {
textView.text = nil
textView.textColor = .black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.textColor = .gray
textView.text = "Chapter Title"
}
}
The problem is that, whatever I type on the text view of the first cell, appears on the 4th cell, I know that this is happening because of dequeueReusableCell but I can't seem to solve this problem.

The problem is that you don't specify what text to appear on each of your CollectionViewCell's textView. As long as you don't specify the same in cellForItemAt indexPath it is going to show the reused cell and its content, from dequeueReusableCell as you said.
For the solution to your specific problem you can do as below in the viewcontroller:
`var textViewContentArray = [Int: String]()` //Create a dictionary to hold the texts globally
In textViewDidEndEditing:
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.textColor = .gray
textView.text = "Chapter Title"
} else {
guard let cell = textView.superview?.superview as? CustomWriterPageCell else {
return
}
guard let indexPath = self.collectionView.indexPath(for: cell) else { return }
textViewContentArray[indexPath.row] = textView.text
}
}
In textViewDidBeginEditing:
func textViewDidBeginEditing(_ textView: UITextView) {
textView.textColor = .black
}
And in cellForItemAt indexPath:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomWriterPageCell", for: indexPath) as! CustomWriterPageCell
cell.textView.delegate = self
let text = textViewContentArray[indexPath.row]
if !text.isEmpty {
cell.textView.textColor = .black
cell.textView.text = text
} else {
cell.textView.textColor = .gray
cell.textView.text = "Chapter Title"
}
return cell
}
Note: Here I am assuming you have set the Controller which holds the collectionView as delegate for the textViewInCell, if the cell is the delegate you can update the textViewContentArray using protocol
Hope this adds to your understanding.

I am assuming that 4th cell does not fit in current view (i.e available below on scroll - i.e Reused cell)
1) If you are reusing cell then you need to ensure that before cell goes out of view you need to save UITextView data somewhere. Otherwise it will lose data (i.e text written) due to reuse. Save data for each cell in data structure/any form.
2) If you scroll to 4th cell then check if data exists. If does not then set empty state to text view. If you write something then save data for 4th cell in data structure/any form..
(You can use this textViewDidEndEditing() to save current data)
3) Again scroll back to 1st cell type something there are save data of 1st cell data structure/any form for that index.
4) Now scroll back to 4th cell. During reuse set saved data of 4th cell to cell text view.
Data Saved at Index == Cell Index
Make sure you handle your use case correctly. There is nothing wrong with the reuse implementation. Make sure you save data and reset data correctly.

I suggest that the part you want to change is encapsulated into a model, such as the textColor, text you mentioned. Don't manually change the UI display, I mean by modifying the model and then refreshing the UI, so that's not There will be repetitions.
This is the rule for all tables, modify the model to refresh the UI, I hope to be useful to you.

Related

Accessing subviews from UITapGestureRecognizer

I have a collection view with cells that are populated based on an array. Each cell has a UIImageView and an UILabel. I want to be able to update the text within the UILabel each time the Cell View itself is tapped. I have the tapping gesture working fine and i can print out the sender information and access the sender view and even get the restoration identifier but I can't seem to access the 'myImage' or 'myLabel' sub views within that cell which is being tapped.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! CardCell
cell.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
let railroadName = railroadNames[indexPath.item]
cell.myImage.image = #imageLiteral(resourceName:railroadName)
cell.myImage.clipsToBounds = true
cell.myImage.layer.cornerRadius = 8
cell.restorationIdentifier = railroadName
cell.myLabel.isHidden = true
cell.myLabel.text = "2"
cell.myLabel.layer.backgroundColor = UIColor(red:0.25, green:0.52, blue:0.96, alpha:1.0).cgColor
cell.myLabel.layer.cornerRadius = 18
cell.myLabel.textColor = UIColor.white
return cell
}
#objc func tap(_ sender: UITapGestureRecognizer) {
//print(sender)
//print(sender.view?.restorationIdentifier)
}
Can't access subviews. Want to take the current text from 'myLabel' and increase the count by 1 each tap.
You can use below code to get label text of cell. but i insist you to manage this in railroadNames array. so, you can save label text in array object's field and can get that field value using indexpath using below code and perform operation on based of your requirement. and then you can reload that particular cell to show effect on tableview.
Also, you should use didSelectItemAt method to get select cell event. if not necessary reason to use UITapGestureRecognizer.
#objc func tap(_ sender: UITapGestureRecognizer) {
//Get Collection view cell indexpath on based of location of tap gesture
let cellPosition = sender.location(in: self.collectionView)
guard let indexPath = self.collectionView.indexPathForItem(at: cellPosition) else{
print("Indexpath not found")
return
}
//Get Ceollection view cell
guard let cell = self.collectionView.cellForItem(at: indexPath) as? CellMainCategoryList else{
print("Could't found cell")
return
}
//Get String of label of collection view cell
let strMyLable = cell. myLabel.text
//you can process addition on based of your requirement here by getting Int from above string.
}
instead of above, you can manage it like below code also:
#objc func tap(_ sender: UITapGestureRecognizer) {
let cellPosition = sender.location(in: self.collectionViewCategaries)
guard let indexPath = self.collectionViewCategaries.indexPathForItem(at: cellPosition) else{
print("Indexpath not found")
return
}
let railroadName = railroadNames[indexPath.item]
let myLableText = railroadName.myLabelValue
//update value
railroadName.myLabelValue = "2"
self.collectionView.reloadItems(at: [indexPath])
}
Hope this helps.

Swift 4 UITableView make one cell as UITextView

I have a dynamic Table as UITableView and all cells are as normal (retrieved form my array)
However I need only one cell as TextView to be able input text. On text Change I need to retrieve the text user input.
How to make this?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count+1 //to allow this input field
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row < array.cont){
//normal cell from array
let cell = Table.dequeueReusableCell(withIdentifier: "myCell")
cell?.textLabel?.text = array[indexPath.row]
cell?.isUserInteractionEnabled = true;
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.textAlignment = .center;
return cell!;
}else{
//create input text field (DON'T KNOW HOW)
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if(indexPath.row < array.cont){
//.. perform action ..
}else{
//retrieve input text (DONT know How)
}
}
Creating UITextView inside UITableViewCell is quite simple :
let textView: UITextView = UITextView(frame: CGRect(x: 0, y: 20, width: 311.00, height: 50.00)) // Set frames as per requirements
textView.textAlignment = NSTextAlignment.justified
cell.contentView.addSubView(textView)
But this would lead to incorrect values while scrolling the table. Best approach would be to create a custom cell and add UITextView there. Here is the custom cell. Keep the constraints intact.
Before using the custom cell, you need to register it in your table. So :
let nib = UINib(nibName: "TextCell", bundle: nil)
Table.register(nib, forCellReuseIdentifier: "TextCell")
Don't forget to put identifier of cell in xib.
Now in cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row < array.cont){
//normal cell from array
let cell = Table.dequeueReusableCell(withIdentifier: "myCell")
cell?.textLabel?.text = array[indexPath.row]
cell?.isUserInteractionEnabled = true;
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.textAlignment = .center;
return cell!;
}else{
//create input text field (DON'T KNOW HOW)
let cell = tableView.dequeueReusableCell(withIdentifier: "TextCell", for: indexPath) as! TextCell
// Access UITextView by cell.textView
return cell
}
}
The main issue is - dynamic cell size as per UITextView height. But that entirely depends on your requirement. You can follow this post for that.
You can achieve this with delegation pattern or NSNotification.
Here's the solution for this using delegation pattern.
Create new UITableViewCell using xib and add the textView on contentView of cell, set the reuse identifier and than register the xib in the ViewController with
tableView.register(UINib(nibName: "name of the xib file", bundle: nil), forCellReuseIdentifier: "Identifier here")
Now define protocol anywhere outside of the class
// You can give any name
// Here we are confirming to class to avoid any retain cycles
protocol CustomCellDelegate :class {
func returnText(text :String)
}
Now initialise " var delegate : CustomCellDelegate? " in same class of UITableViewCell that we created above while creating xib.
and confirm to protocol UITextViewDelegate and than in the cell class write this
override func awakeFromNib() {
super.awakeFromNib()
textView.delegate = self
}
after that add these functions in same class of tableViewCell
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(text == "\n") {
textView.resignFirstResponder()
return false
}
return true
}
func textViewDidEndEditing(_ textView: UITextView) {
delegate.returnText(text : textView.text ?? "")
}
Now in the ViewController class
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == array.count { // this will call the cell with TextView at the end , you can play with any indexPath to call the cell
let cell = tableView.de... // Deque the cell with TextView here using reuse identifier
cell.delegate = self
return cell
}
else {
// deque other cells
}
}
we'll write an extension of ViewController and confirm to our custom protocol
extension ViewController : CustomCellDelegate {
// this function will get called when you end editing on textView
func returnText(text :String) {
print(text) // you may save this string in any variable in ViewController class
}
}
Add a TextView in to your custom cell, hide it, show when you need
if you want to have the textView on the top of all cells, drag and drop a UIView inside the tableView before the cell.
this view will scroll with cells.
design this view as you need insert a textView inside it, and use textView's delegate methods to perform operations.

UITableView Separators and Disclosure Indicator missing after altering UITableViewCellActionButton

with the help of this Thread I managed to alter the font of my UITableViewCellActionButtons, but now the line-separator for each cell only gets visible after swiping the cell once and the disclosure indicators never show up.
Here is my extension-code:
extension UITableViewCell{
override open func layoutSubviews() {
super.layoutSubviews()
for subview in self.subviews {
for sub in subview.subviews {
if String(describing: sub).range(of: "UITableViewCellActionButton") != nil {
for view in sub.subviews {
if String(describing: view).range(of: "UIButtonLabel") != nil {
if let label = view as? UILabel {
label.font = UIFont(name: "CaptureSmallz", size: label.font.pointSize)
}
}
}
}
}
}
}
}
My cellforRowAt looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "planNameCell", for: indexPath)
...
cell.textLabel?.text = ...
cell.detailTextLabel?.text = ...
cell.selectionStyle = UITableViewCellSelectionStyle.none
let textLabelFontSize = cell.textLabel?.font.pointSize
cell.textLabel?.font = UIFont(name: "CaptureSmallz", size: textLabelFontSize!)!
cell.textLabel?.sizeToFit()
let detailLabelFontSize = cell.detailTextLabel?.font.pointSize
cell.detailTextLabel?.font = UIFont(name: "CaptureSmallz", size: detailLabelFontSize!)!
cell.detailTextLabel?.sizeToFit()
return cell
}
I hope somebody knows what I messed up here.
Thanks for the help!
//Edit 1:
I've checked the view hierachy with the Xcode-Debugger, but it shows only a blank screen. But I can see the hierachy on the left side:
see this screenshot
// this is the state of the app I debugged
I noticed that there arent any views for the disclosure indicators and when I swipe left on a cell once the line separator appears and one _UITableViewCellSeparatorView entry gets added in the view hierachy. But I still don't know where to look now.
//Edit 2:
I'm currently trying to get rid of the extension and call the code in it manually, but I can`t figure it out where to put it. I tried "viewDidAppear", but then the "UITableViewCellActionButton" aren't there yet. Is there a way to call function right after "editActionsForRowAt" has been called?

Swift UICollectionView Cells Mysteriously Repeat

We are trying to make a collection view. In each cell the users can choose an image and enter text into a text field. We noticed that after adding four cells, when we add a new cell, the text field is already filled with the information from previous cells. In our code, we never programmatically fill the text field (which starts out empty), we allow the user to do this. Any suggestions?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Image", forIndexPath: indexPath) as! CollectionViewCell
cell.deleteButton?.addTarget(self, action: #selector(AddNewItem.xButtonPressed(_:)), forControlEvents: UIControlEvents.TouchUpInside)
cell.deleteButton?.layer.setValue(indexPath.row, forKey: "index")
let item = items[indexPath.item]
let path = getDocumentsDirectory().stringByAppendingPathComponent(item.image)
cell.imageView.image = UIImage(contentsOfFile: path)
cell.imageView.layer.borderColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3).CGColor
cell.imageView.layer.borderWidth = 2
cell.layer.cornerRadius = 7
return cell
}
You can use this in UICollectionViewCell custom class
override func prepareForReuse() {
self.profileImg.image = #imageLiteral(resourceName: "Profile Icon Empty")
super.prepareForReuse()
}
Problem is that you are using dequeReusableCellWithIdentifier which returns already created cell(that you were using before). That's why it's already filled with previous data. You need to clear this data before showing this cell, or fill it from some storage(for example array that represents your collection view cells(each object in array somehow related to cell, in your case that is text wroten in cell))
Here's how I ultimately ended up resolving it.
I created an Item class which contained all of the fields which are shown in the collection view cell and created an array of Items.
Here is a simplified version of my CollectionViewCell class, which here only has a single text field:
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var itemName: UITextField!
var item: Item?
func initializeListeners(){
itemName.addTarget(self, action: #selector(itemNameChanged(_:)), forControlEvents: UIControlEvents.EditingChanged)
}
//When the item name is changed, make sure the item's info is updated
func itemNameChanged(textField: UITextField) {
item?.itemName = textField.text!
}
}
Here's a simplified version of the cellForItemAtIndexPath function in my view controller class:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! CollectionViewCell
cell.initializeListeners()
let item = items[indexPath.item]
cell.item = item
cell.itemName.text = item.itemName
return cell
}
The reason is that collectionViewLayout.collectionViewContentSize.height
is taller than the real contents size! It is recommended to keep UICollectionView calculate the height automatically (without using UIScrollView, let UICollectionView maintain the scroll), as manual change will cause lots of weird behaviors.

PrepareForReuse in CollectionViewCell

I want to hide the label in a cell that was tapped and instead show an image. But I want to do this only if a cell with a certain index has already been set to the imageView.
What is the best way to address the cells and store if they are set to imageView or not? How do I use the prepareForReuse method?
This is the way I do it until now, but as the cells are reused. The image is shown in other cells at scrolling.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("user tapped on door number \(indexPath.row)")
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! MyCollectionViewCell
if (cell.myLabel.text == "1") {
one = true
if(seven = true) {
if (cell.myLabel.hidden) {
cell.myLabel.hidden = false
cell.MyImageView.image = nil
}
else {
cell.myLabel.hidden = true
cell.MyImageView.image = UIImage(named:"1")!
}
}
}
You didn't say if your collection view has exactly 7 cells or if it can have "N" (e.g. 100) cells in the collection, so if this were my problem and I had to solve it, I would make the state of your "seven" cell a property of the class (e.g. "var sevenState : Bool") and then I could display the button or image of other cells depending on what sevenState is.
In my app I have to configure a UICollectionReusableView based on the index path, if the indexPath has a particular value then I send an array which is used to set labels and images.
I use a function in the custom UICollectionReusableView, if I call it with an array it populates the labels and images and if I call it with nil it resets these.
func collectionView(collectionView: UICollectionView!, viewForSupplementaryElementOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> UICollectionReusableView! {
.... [logic around selecting index path based on data returned]
....
if filteredEvents != nil{
reusableView.populateCalendarDayDates(sortedEvents)
}else{
reusableView.populateCalendarDayDates(nil)
}
In the function in the custom UICollectionReusableView I reset labels back to default values before possibly updating them :
func populateCalendarDayDates(arrayEvents: NSArray?){
let firstDayTag = tagStartDay()
var dayDate = 1
for var y = 1; y < 43; y++ {
let label = self.viewWithTag(y) as! BGSCalendarMonthLabel
label.delegate = callingCVC
label.backgroundColor = UIColor.clearColor()
label.textColor = UIColor.whiteColor()
label.text = ""
You can get the same effect, and it is probably a bit more readable, by moving this code to prepareForReuse in the custom UICollectionReusableView :
override func prepareForReuse() {
super.prepareForReuse()
for var y = 1; y < 43; y++ {
let label = self.viewWithTag(y) as! BGSCalendarMonthLabel
label.backgroundColor = UIColor.clearColor()
label.textColor = UIColor.whiteColor()
label.text = ""
}
}
Hope that helps.

Resources