I have a UICollectionView with 4 custom UICollectionViewCells. In the header of the UICollectionView there's a UISegmentedControl. My goal is to change the header UILabel that plays the role of a title. Right now if the segmented control value had been changed, the cells are reloaded and the title should be switched, but it overlaps with the first title. I can't figure out why.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionElementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderDiscoverVC", for: indexPath) as! HeaderDiscoverVC
headerView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: 30)
headerView.backgroundColor = UIColor.hex("d9e2e7")
let label = UILabel(frame: CGRect(x: 16, y: 0, width: headerView.frame.width, height: 30))
switch segReusableIdentifier {
case "Reply":
label.text = "Reply"
case "Media":
label.text = "Media"
case "Likes":
label.text = "Likes"
case "Comments":
label.text = "Comments"
default:
label.text = ""
}
label.font = UIFont(name: Fonts.OpenSans_Bold, size: 16)
label.textColor = UIColor.hex("8a9da6")
headerView.addSubview(label)
return headerView
}
fatalError("Unexpected element kind")
}
The problem is with the way you are adding the label to your header view.
You should put the headerView.addSubview(label) to your HeaderDiscoverVC Class. Also set the colour and font to the same class.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionElementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderDiscoverVC", for: indexPath) as! HeaderDiscoverVC
headerView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: 30)
// MOVE THE COMMENTED LINE TO YOUR HeaderDiscoverVC
//headerView.backgroundColor = UIColor.hex("d9e2e7")
headerView.label.frame = CGRect(x: 16, y: 0, width: headerView.frame.width, height: 30)
switch segReusableIdentifier {
case "Reply":
headerView.label.text = "Reply"
case "Media":
headerView.label.text = "Media"
case "Likes":
headerView.label.text = "Likes"
case "Comments":
headerView.label.text = "Comments"
default:
headerView.label.text = ""
}
// MOVE THE COMMENTED LINES TO YOUR HeaderDiscoverVC
//label.font = UIFont(name: Fonts.OpenSans_Bold, size: 16)
//label.textColor = UIColor.hex("8a9da6")
//headerView.addSubview(label)
return headerView
}
fatalError("Unexpected element kind")
}
Try and share your results
You are adding label programmatically to the headerView which should be removed before adding again. dequeueReusableSupplementaryView do not remove programmatically added subviews.
In your code:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionElementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderDiscoverVC", for: indexPath) as! HeaderDiscoverVC
headerView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: 30)
headerView.backgroundColor = UIColor.hex("d9e2e7")
///// Add below code to remove all subviews first before adding any new subview programmatically
for label in headerView.subviews {
if let mylabel = label as? UILabel {
mylabel.removeFromSuperview()
}
}
////////////////
let label = UILabel(frame: CGRect(x: 16, y: 0, width: headerView.frame.width, height: 30))
switch segReusableIdentifier {
case "Reply":
label.text = "Reply"
case "Media":
label.text = "Media"
case "Likes":
label.text = "Likes"
case "Comments":
label.text = "Comments"
default:
label.text = ""
}
label.font = UIFont(name: Fonts.OpenSans_Bold, size: 16)
label.textColor = UIColor.hex("8a9da6")
headerView.addSubview(label)
return headerView
}
fatalError("Unexpected element kind")
}
Better approach will be to keep the label in HeaderDiscoverVC and use it in the code as:
headerView.label.text = "Your data"
In this way you don't have to remove subViews programmatically.
Related
I have a custom tableView, entirely created in code. IE, the cells need to be in code too.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "inventoryCell", for: indexPath)
let namelabel = UILabel()
namelabel.frame = CGRect(x: 145, y: 10 , width: 200, height: 30 )
namelabel.text = itemToShow.name
namelabel.font = UIFont.boldSystemFont(ofSize: 20)
namelabel.backgroundColor = .clear
cell.addSubview(namelabel)
let detailLabel = UILabel()
detailLabel.frame = CGRect(x: 145, y: 50 , width: 200, height: 50 )
detailLabel.text = itemToShow.detail
detailLabel.backgroundColor = .clear
cell.addSubview(detailLabel)
let inventoryImage = UIImageView()
inventoryImage.frame = CGRect(x: 10, y: 10, width: 130, height: 130)
inventoryImage.image = UIImage(named: "emptyInventorySlot")
cell.addSubview(inventoryImage)
return cell
}
It works great, and you can see it loads perfectly. The top image is the load, the bottom image is once I scroll to the bottom. You can see the text labels seem to all stack on top of each other.
I would not recommend doing it this way.
It's better to move the code that creates namelabel, detailLabel and inventoryImage to an inventoryCell class derived from UITableViewCell.
But you can make it work.
Since table cells are reused and the reused cells already contain the created subviews, you need to either remove the subviews or treat cells with already created subviews differently.
And you should place your labels in the contentView of your UITableCellView.
To make your code work you can do this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "inventoryCell", for: indexPath)
// remove any subviews from contentView from recycled cells
cell.contentView.subviews.forEach{ $0.removeFromSuperview() }
let namelabel = UILabel()
namelabel.frame = CGRect(x: 145, y: 10 , width: 200, height: 30 )
namelabel.text = itemToShow.name
namelabel.font = UIFont.boldSystemFont(ofSize: 20)
namelabel.backgroundColor = .clear
// add any label to contentView of the cell
cell.contentView.addSubview(namelabel)
let detailLabel = UILabel()
detailLabel.frame = CGRect(x: 145, y: 50 , width: 200, height: 50 )
detailLabel.text = itemToShow.detail
detailLabel.backgroundColor = .clear
// add any label to contentView of the cell
cell.contentView.addSubview(detailLabel)
let inventoryImage = UIImageView()
inventoryImage.frame = CGRect(x: 10, y: 10, width: 130, height: 130)
inventoryImage.image = UIImage(named: "emptyInventorySlot")
// add any label to contentView of the cell
cell.contentView.addSubview(inventoryImage)
return cell
}
Better would be:
create a UITableViewCell subclass
place your label placement and configuration there
take care of cell reuses, override prepareForReuse() to support cell reuse
register this class as a table view cell for your tableView
cast your dequeued cell to your UITableViewCell subclass
I have the following code
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
//creating a cell using the custom class
if (indexPath.row == 0 && indexPath.section == 0) {
var cell:UITableViewCell;
cell = tableView.dequeueReusableCell(withIdentifier: "book", for: indexPath)
cell.selectionStyle = UITableViewCell.SelectionStyle.none;
self.ref.observe(DataEventType.value, with: { (snapshot) in
self.ref.child("featured").observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let title = value?["title"] as? String ?? ""
self.featured = value?["url"] as? String ?? ""
let label = UILabel(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: 24))
label.numberOfLines = 0
label.font = UIFont(name: "CeraPro-Medium", size: 20);
label.textColor = UIColor.black
label.text = NSLocalizedString(" Free Daily Pick", comment: "featured")
label.adjustsFontForContentSizeCategory = true
cell.addSubview(label)
if let image = value?["image"] as? String, image.isEmpty {
print("String is nil or empty.")
} else {
if let image = value?["image"] {
let imageView = UIImageView();
imageView.frame = CGRect(x: 0,
y: 75,
width: cell.frame.size.width,
height: 150)
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false
imageView.sd_setImage(with: URL(string: image as! String), placeholderImage: nil)
cell.addSubview(imageView)
let label = UILabel(frame: CGRect(x: 10,
y: cell.frame.size.height-60,
width: cell.frame.size.width,
height: 50));
label.textColor = UIColor.black
label.backgroundColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 0.5)
label.font = UIFont(name: "CeraPro-Bold", size: 16)
label.text = " \(title)"
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: 150);
view.tag = 123
view.addSubview(imageView)
view.addSubview(label)
cell.contentView.addSubview(view)
}
}
// ...
}) { (error) in
print(error.localizedDescription)
}
})
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "book", for: indexPath) as! BookTableViewCell
cell.cellIndex = indexPath
cell.dataSource = self
cell.delegate = self
cell.backgroundColor = UIColor.white
var books = [Book]();
let count = self.enterpriseBooks.count;
if count > 0 && indexPath.section <= self.enterpriseBooks_2.count {
books = self.enterpriseBooks_2[indexPath.section - 1]!;
}
if (indexPath.section == (count + 1)) {
books = nytbooks;
} else if (indexPath.section == (count + 2)) {
books = trendingbooks;
} else if (indexPath.section == (count + 3)) {
books = newbooks
}
if (books.count > 0) {
if (cell.collectionView === nil) {
cell.addCollectionView();
cell.collectionView.reloadData()
}
}
return cell
}
}
Here for section 0, row 0 - I show a featured image
For section 1, 2, 3 - I show a horizontal collection view of book images.
What's happening is by using dequeue function to get the table view cell, the table view is caching some table view cell content which overlaps each other. Is there a way to purge table view cells before cellForRowAt is called?
Programmatically adding subviews to a cell seems a terrible idea to me. Try to use auto layout and xib files whenever possible.
Anyway, you theoretically could remove all the subviews from a dequeued cell and then adding them again.
for view in cell.subviews {
view.removeFromSuperview()
}
That said, there are better approaches to use a special cell in the first position, like using different cell types.
well, to handle caching data you have to handle all else cases to assign appropriate values to UI Elements in case of nil/ failure scenarios. For an instance you have to assign nil to UIImageView's image property if image url is nil/ empty like this
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
//creating a cell using the custom class
if (indexPath.row == 0 && indexPath.section == 0) {
var cell:UITableViewCell;
cell = tableView.dequeueReusableCell(withIdentifier: "book", for: indexPath)
cell.selectionStyle = UITableViewCell.SelectionStyle.none;
self.ref.observe(DataEventType.value, with: { (snapshot) in
self.ref.child("featured").observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let title = value?["title"] as? String ?? ""
self.featured = value?["url"] as? String ?? ""
let label = UILabel(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: 24))
label.numberOfLines = 0
label.font = UIFont(name: "CeraPro-Medium", size: 20);
label.textColor = UIColor.black
label.text = NSLocalizedString(" Free Daily Pick", comment: "featured")
label.adjustsFontForContentSizeCategory = true
cell.addSubview(label)
let imageView = UIImageView();
if let image = value?["image"] as? String, image.isEmpty {
print("String is nil or empty.")
imageView.image = nil
// same goes for the label
} else {
if let image = value?["image"] {
imageView.frame = CGRect(x: 0,
y: 75,
width: cell.frame.size.width,
height: 150)
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false
imageView.sd_setImage(with: URL(string: image as! String), placeholderImage: nil)
cell.addSubview(imageView)
let label = UILabel(frame: CGRect(x: 10,
y: cell.frame.size.height-60,
width: cell.frame.size.width,
height: 50));
label.textColor = UIColor.black
label.backgroundColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 0.5)
label.font = UIFont(name: "CeraPro-Bold", size: 16)
label.text = " \(title)"
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: 150);
view.tag = 123
view.addSubview(imageView)
view.addSubview(label)
cell.contentView.addSubview(view)
}
}
// ...
}) { (error) in
print(error.localizedDescription)
}
})
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "book", for: indexPath) as! BookTableViewCell
cell.cellIndex = indexPath
cell.dataSource = self
cell.delegate = self
cell.backgroundColor = UIColor.white
var books = [Book]();
let count = self.enterpriseBooks.count;
if count > 0 && indexPath.section <= self.enterpriseBooks_2.count {
books = self.enterpriseBooks_2[indexPath.section - 1]!;
}
if (indexPath.section == (count + 1)) {
books = nytbooks;
} else if (indexPath.section == (count + 2)) {
books = trendingbooks;
} else if (indexPath.section == (count + 3)) {
books = newbooks
}
if (books.count > 0) {
if (cell.collectionView === nil) {
cell.addCollectionView();
cell.collectionView.reloadData()
}
}
return cell
}
}
In all the nil cases you are doing nothing with related UI element and that allows dequeueReusability to retain old content. Just assign nil or default value to UI elements in else / nil case, it will resolve your issue
I'm trying to generate cells and put labels inside of it. However, when i scroll down labels got mixed between cells. Here is my code and i'm trying to solve it.
let lblTitle = UILabel()
let lblMetro = UILabel()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuCell", for: indexPath) as? UICustomCollectionViewCell
if indexPath.row == 0 {
lblTitle.frame = CGRect(x: 0, y: 0, width: 195, height: 40)
lblTitle.font = UIFont.systemFont(ofSize: 14)
lblTitle.textColor = UIColor.white
lblTitle.backgroundColor = colorLiteral(red: 0.2122299671, green: 0.4379466176, blue: 0.8993332386, alpha: 1)
lblTitle.text = " 1”
cell?.contentView.addSubview(lblTitle)
}
if indexPath.row == 1 {
lblMetro.frame = CGRect(x: 55, y: 290, width: 100, height: 20)
lblMetro.font = UIFont.boldSystemFont(ofSize: 17)
lblMetro.textColor = colorLiteral(red: 0, green: 0.3117707968, blue: 0.5609284043, alpha: 1)
lblMetro.text = “2”
cell?.contentView.addSubview(lblMetro)
}
return cell ?? UICollectionViewCell()
}
}
Here cells are dequeued
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuCell", for: indexPath) as? UICustomCollectionViewCell
so you might get a 1 with previously added label , you need to clear them after dequeue , it would be messy but it's better to isolate the vc's labels from the cells 1 so add them inisde the cell configure or make them an outlets , to remove give them a tag and after the above line do
cell.contentView.subviews.forEach {
if $0.tag == 200 {
$0.removeFromSuperview()
}
}
Not optimised but this might solve it, remove the subview from superview before adding it:
cell?.contentView.lblTitle.removeFromSuperview()
cell?.contentView.addSubview(lblTitle)
And:
cell?.contentView.lblMetro.removeFromSuperview()
cell?.contentView.addSubview(lblMetro)
I suggest using a very rarely used method of UICollectionViewCell or UITableViewCell prepareForReuse.
In definition of UICustomCollectionViewCell insert the function:
class UICustomCollectionViewCell: UICollectionViewCell {
func prepareForReuse() {
// This method is immediately called when a cell is about to be dequeued.
super.prepareForReuse()
if let view = contentView.viewWithTag(100) {
view.removeFromSuperView()
}
if let view = contentView.viewWithTag(101) {
view.removeFromSuperView()
}
}
}
Then give tags to the labels
lblMetro.tag = 100
lblTitle.tag = 101
This solution is efficient if you only use a limited labels and cells. For a more generic approach create labels dynamically and share the tag. In the prepareForReuse() just remove subview with that tag.
I want to add multiple UILabels programmatically side by side to a TableViewCell. The UILabels have different width.
The first cell in the picture shows the problem and the second cell what I would like to do.
In this example I want to add four UILabels to a TableViewCell. But the width of the TableViewCell is smaller than the width of the UILabels. Therefore I must increase the CellHeight and add the UILabels below to the other UILabels (like the second cell in the picture).
You should put UICollectionView inside of a row of your UITableViewCell.
Each cell of your UICollectionView will have a multi UILabel. Update the dataSource for your UICollectionView depending on your label count. Set isScrollEnabled false of that UICollectionView and set automatic row height for UITableViewCell.
Also, set flow layout to UICollectionView :
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSizeMake(1, 1) }
Adjust cell size like below:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let size: CGSize = keywordArray[indexPath.row].size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14.0)])
return CGSize(width: size.width + 45.0, height: keywordsCollectionView.bounds.size.height)
}
At first you have to make for labels.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
let label1 = UILabel(frame: CGRect(x: 5, y: 5, width: 100, height: 25))
label1.text = "Label 1"
let label2 = UILabel(frame: CGRect(x: 110, y: 5, width: 225, height: 25))
label2.text = "Label 2"
let label3 = UILabel(frame: CGRect(x: 5, y: 40, width: 100, height: 25))
label3.text = "Label 3"
let label4 = UILabel(frame: CGRect(x: 110, y: 40, width: 225, height: 25))
label4.text = "Label 4"
cell.addSubview(label1)
cell.addSubview(label2)
cell.addSubview(label3)
cell.addSubview(label4)
return cell
}
Each label has different width and different position. You can play with it
Have custom footer view in my UITableView:
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { // custom view for footer. will be adjusted to default or specified footer height
return footer()
}
func footer() -> UILabel {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: (navigationController?.navigationBar.bounds.size.height)!))
label.backgroundColor = AppColors.Bordo.color
label.font = UIFont.boldSystemFont(ofSize: 16)
label.textColor = .white
label.text = "Selected \(self.selectedGenres.count) of \(self.genres.count)"
label.textAlignment = .center
return label
}
When user select/deselect row in table view I want refresh my footer with info of selected rows. How I can do it without reloading whole tableview?
And what method footerView(forSection: indexPath.section) of UITableView does?
create a global object of label... and init it only when the label is nil... and you can access this label anywhere in code.
let globalLabel : UILabel ?
public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { // custom view for footer. will be adjusted to default or specified footer height
return footer()
}
func footer() -> UILabel {
if (globalLabel == nil) {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: (navigationController?.navigationBar.bounds.size.height)!))
label.backgroundColor = AppColors.Bordo.color
label.font = UIFont.boldSystemFont(ofSize: 16)
label.textColor = .white
label.textAlignment = .center
globalLabel = label
}
globalLabel.text = "Selected \(self.selectedGenres.count) of \(self.genres.count)"
return globalLabel
}
Done it using Rajesh Choudhary proposal and computing property:
var selectedGenres: [Genre] = [] {
didSet {
self.footerForTableView.text = titleForFooter
}
}
var titleForFooter: String {
return "Selected \(self.selectedGenres.count) of \(self.genres.count)"
}
lazy var footerForTableView: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: (self.navigationController?.navigationBar.bounds.size.height)!))
label.backgroundColor = AppColors.Bordo.color
label.font = UIFont.boldSystemFont(ofSize: 16)
label.textColor = .white
label.text = self.titleForFooter
label.textAlignment = .center
return label
}()