UITableViewCell gets reused and shows wrong drawing in UIView - ios

I've been having this problem for weeks and I think I am not able to solve it.
I have a tableView which has custom tableViewCells and they are filled with data. The tableViewCell has two UILabels and one UIView.
The problem appears when I scroll several times the drawings on the UIViews are overlapped one with another, I think they are redrawn. I know that this behavior is because of the reuse of the cells but I can't even locate the origin of the problem.
UIViews show perfectly when opening the app
UIViews get overlapped after scrolling
My UITableView's cellForRowAtIndexPath is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let CellIdentifier = "Cell"
let cell: CustomTableViewcell = self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! CustomTableViewcell
//self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier) as! CustomTableViewcell
cell.prepareForReuse()
self.createUIForCell(cell)
self.configureCell(cell, indexPath: indexPath)
print("\t\(parsedData[indexPath.row].stationName): \(parsedData[indexPath.row].freeBikes)")
return cell
}
func createUIForCell(cell: CustomTableViewcell) {
cell.distanceLabel.textColor = UIColor.whiteColor()
cell.distanceLabel.backgroundColor = UIColor.clearColor()
cell.bikeStationLabel.textColor = UIColor.whiteColor()
cell.bikeStationLabel.backgroundColor = UIColor.clearColor()
}
func configureCell(cell: CustomTableViewcell, indexPath: NSIndexPath) {
let stations = parsedData[indexPath.row]
if indexPath.row % 2 == 0 {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor1
cell.progressView.backgroundColor = cellBackgroundColor2
} else {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor2
cell.progressView.backgroundColor = cellBackgroundColor1
}
cell.progressView.getWidth(stations.freeBikes, freeDocks: stations.freeDocks)
cell.getLocation(stations.latitude, longitude: stations.longitude)
cell.getParameters(stations.freeBikes, freeDocks: stations.freeDocks)
cell.bikeStationLabel.text = stations.stationName
if stations.distanceToLocation != nil {
if stations.distanceToLocation! < 1000 {
cell.distanceLabel.text = String(format: "%.f m", stations.distanceToLocation!)
} else {
cell.distanceLabel.text = String(format: "%.1f km", stations.distanceToLocation! / 1000)
}
} else {
cell.distanceLabel.text = ""
}
}
For the before mentioned UIVIew inside my custom cell I have created a separated class for it to handle the drawing and it looks like this:
import UIKit
class ProgressViewBar: UIView {
var freeBikes = 0
var freeDocks = 0
var text: UILabel? = nil
var text2: UILabel? = nil
func getWidth(freeBikes: Int, freeDocks: Int) {
self.freeBikes = freeBikes
self.freeDocks = freeDocks
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))), height: frame.height), cornerRadius: 0)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(shapeLayer)
drawText(rect)
}
func drawText(rect: CGRect) {
if freeBikes != 0 {
text?.removeFromSuperview()
text = UILabel(frame: CGRect(x: 2, y: 0, width: 20, height: 20))
text!.text = String("\(freeBikes)")
text!.textAlignment = NSTextAlignment.Left
text!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text!.backgroundColor = UIColor.clearColor()
text!.textColor = UIColor(red: 1/255, green: 87/255, blue: 155/255, alpha: 1)
self.addSubview(text!)
}
text2?.removeFromSuperview()
text2 = UILabel(frame: CGRect(x: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))) + 2, y: 0, width: 20, height: 20))
text2!.text = String("\(freeDocks)")
text2!.textAlignment = NSTextAlignment.Left
text2!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text2!.backgroundColor = UIColor.clearColor()
text2!.textColor = UIColor.whiteColor()
self.addSubview(text2!)
}
}
The view needs two parameters to be drawn and I pass them using the func getWidth(freeBikes: Int, freeDocks: Int) method calling it from the `ViewController. If any other piece of code is needed you can look at the repo.

The problem is that when reusing the cell you call drawRect once again and don't clear the already drawn rect. Clear it before drawing another one.

Related

Animating CALayer is not working on iOS 14

I have a problem with animating CALayer after updating to Xcode12 and iOS14 - animations that used to work on iOS13 are not working on iOS14. Here is the code for animations:
private func animateCellPress() {
let lightShadowAnimation = CABasicAnimation(keyPath: "shadowOffset")
lightShadowAnimation.fromValue = CGSize(width: -self.shadowRadius, height: -self.shadowRadius)
lightShadowAnimation.toValue = CGSize(width: self.shadowRadius, height: self.shadowRadius)
lightShadowAnimation.fillMode = .forwards;
lightShadowAnimation.isRemovedOnCompletion = false;
lightShadowAnimation.duration = self.animationDuration
self.lightShadow.add(lightShadowAnimation, forKey: lightShadowAnimation.keyPath)
let darkShadowAnimation = CABasicAnimation(keyPath: "shadowOffset")
darkShadowAnimation.fromValue = CGSize(width: self.shadowRadius, height: self.shadowRadius)
darkShadowAnimation.toValue = CGSize(width: -self.shadowRadius, height: -self.shadowRadius)
darkShadowAnimation.fillMode = .forwards;
darkShadowAnimation.isRemovedOnCompletion = false;
darkShadowAnimation.duration = self.animationDuration
self.darkShadow.add(darkShadowAnimation, forKey: darkShadowAnimation.keyPath)
}
lightShadow and darkShadow are stored as a properties and here is the initialisation code for them:
private func initializeDarkShadow() {
darkShadow = CALayer()
darkShadow.frame = bounds
darkShadow.backgroundColor = backgroundColor?.cgColor
darkShadow.cornerRadius = cornerRadius
darkShadow.shadowOffset = CGSize(width: shadowRadius, height: shadowRadius)
darkShadow.shadowOpacity = 1
darkShadow.shadowRadius = shadowRadius
darkShadow.shadowColor = Asset.Colors.darkShadow.color.cgColor
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: bounds.size)
darkShadow.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(darkShadow, at: 0)
}
private func initializeLightShadow() {
lightShadow = CALayer()
lightShadow.frame = bounds
lightShadow.backgroundColor = backgroundColor?.cgColor
lightShadow.cornerRadius = cornerRadius
lightShadow.shadowOffset = CGSize(width: -shadowRadius, height: -shadowRadius)
lightShadow.shadowOpacity = 1
lightShadow.shadowRadius = shadowRadius
lightShadow.shadowColor = Asset.Colors.lightShadow.color.cgColor
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: bounds.size)
lightShadow.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(lightShadow, at: 0)
}
Any ideas on what's wrong and how can I fix it? Also I am not getting any compiler errors or warnings. Console log is empty as well.
This works. I think each time you are calling your animate layoutSubviews is being called thus removing the layer and the animation. You could either just update the frame there or check if the layer has already been added. If that is not the issue then there is something wrong with you press or touch function. Create a project with a single view and copy and paste this into your ViewController file.
import UIKit
class TableViewCell : UITableViewCell{
let darkShadowColor = UIColor.green
let lightShadowColor = UIColor.red
let animationDuration : Double = 2
var darkShadow = CALayer()
var lightShadow = CALayer()
let cornerRadius : CGFloat = 4
let shadowRadius : CGFloat = 3
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initializeDarkShadow()
initializeLightShadow()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initializeDarkShadow()
initializeLightShadow()
}
private func initializeDarkShadow() {
darkShadow = CALayer()
darkShadow.frame = bounds
darkShadow.backgroundColor = backgroundColor?.cgColor
darkShadow.cornerRadius = cornerRadius
darkShadow.shadowOffset = CGSize(width: shadowRadius, height: shadowRadius)
darkShadow.shadowOpacity = 1
darkShadow.shadowRadius = shadowRadius
darkShadow.shadowColor = darkShadowColor.cgColor
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: bounds.size)
darkShadow.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(darkShadow, at: 0)
}
private func initializeLightShadow() {
lightShadow = CALayer()
lightShadow.frame = bounds
lightShadow.backgroundColor = backgroundColor?.cgColor
lightShadow.cornerRadius = cornerRadius
lightShadow.shadowOffset = CGSize(width: -shadowRadius, height: -shadowRadius)
lightShadow.shadowOpacity = 1
lightShadow.shadowRadius = shadowRadius
lightShadow.shadowColor = lightShadowColor.cgColor
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: bounds.size)
lightShadow.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(lightShadow, at: 0)
}
func animateCellPress() {
print("called")
let lightShadowAnimation = CABasicAnimation(keyPath: "shadowOffset")
lightShadowAnimation.fromValue = CGSize(width: -self.shadowRadius, height: -self.shadowRadius)
lightShadowAnimation.toValue = CGSize(width: self.shadowRadius, height: self.shadowRadius)
lightShadowAnimation.beginTime = CACurrentMediaTime()
lightShadowAnimation.fillMode = .both;
lightShadowAnimation.isRemovedOnCompletion = false;
lightShadowAnimation.duration = self.animationDuration
self.lightShadow.add(lightShadowAnimation, forKey: nil)
let darkShadowAnimation = CABasicAnimation(keyPath: "shadowOffset")
darkShadowAnimation.fromValue = CGSize(width: self.shadowRadius, height: self.shadowRadius)
darkShadowAnimation.toValue = CGSize(width: -self.shadowRadius, height: -self.shadowRadius)
darkShadowAnimation.beginTime = CACurrentMediaTime()
darkShadowAnimation.fillMode = .both;
darkShadowAnimation.isRemovedOnCompletion = false;
darkShadowAnimation.duration = self.animationDuration
self.darkShadow.add(darkShadowAnimation, forKey: nil)
}
override func layoutSubviews() {
super.layoutSubviews()
self.lightShadow.frame = self.bounds
self.darkShadow.frame = self.bounds
}
}
class ViewController: UIViewController {
let identifier = "someCellID"
lazy var tableView : UITableView = {
let tv = UITableView(frame: self.view.bounds)
tv.delegate = self
tv.dataSource = self
tv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
tv.register(TableViewCell.self, forCellReuseIdentifier: identifier)
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.addSubview(tableView)
}
}
extension ViewController : UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? TableViewCell{
cell.selectionStyle = .none
return cell
}
print("error")
return UITableViewCell()
}
func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? TableViewCell{
cell.animateCellPress()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
}

Adding horizontal line below cell in uicollectionview

I am implementing like a calendar layout with some modification which is shown in the screenshot. To achieve this I have used UICollectionView. The problem is, I have to draw a screen width continuous line(green line in screenshot). The green line should cover the whole width, I know its not showing over the circular cell due to half of the cornerRadius and a vertical line only after the first cell(10 am). Where i have to add the shapelayer, so that it ll seems like a continuous line. Here is the code which I have tried so far.
KKViewController.m
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! KKBookCollectionViewCell
self.bookingCollectionView.backgroundColor = UIColor.whiteColor()
let rectangularRowIndex:NSInteger = indexPath.row % 5
if(rectangularRowIndex == 0 )
{
cell.userInteractionEnabled = false
cell.layer.cornerRadius = 0
cell.timeSlotLabel.text = "10am"
cell.backgroundColor = UIColor.whiteColor()
cell.layer.borderWidth = 0
cell.layer.borderColor = UIColor.clearColor().CGColor
}
else
{
cell.userInteractionEnabled = true
cell.layer.cornerRadius = cell.frame.size.width/2
cell.timeSlotLabel.text = ""
//cell.backgroundColor = UIColor.lightGrayColor()
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.grayColor().CGColor
if cell.selected == true
{
cell.backgroundColor = UIColor.greenColor()
}
else
{
cell.backgroundColor = UIColor.lightGrayColor()
}
}
return cell
}
KKCollectionCell.m
var borderWidth:CGFloat!
var borderPath:UIBezierPath!
override func awakeFromNib() {
super.awakeFromNib()
drawHorizontalLine()
dottedLine(with: borderPath, and: borderWidth)
drawVerticalLine()
dottedLine(with: borderPath, and: borderWidth)
func drawVerticalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y))
//borderPath.addLineToPoint(CGPointMake(self.frame.size.width - 5, self.frame.origin.y + self.frame.size.height - 50))
borderPath.addLineToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func drawHorizontalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(0, self.frame.origin.y))
borderPath.addLineToPoint(CGPointMake(self.frame.size.width + 10, self.frame.origin.y))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func dottedLine (with path:UIBezierPath, and borderWidth:CGFloat)
{
let shapeLayer = CAShapeLayer()
shapeLayer.strokeStart = 0.0
shapeLayer.strokeColor = UIColor.greenColor().CGColor
shapeLayer.lineWidth = borderWidth
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [1,2]
shapeLayer.path = path.CGPath
self.layer.addSublayer(shapeLayer)
}
You can add a new view inside the collection view cell and set the corner radius for that new view. Also you have to reduce the spacing between the cells. Then the line will look like what you expected.
I know it's an old question but it was never properly answered, i was looking for such behaivior and achieved it with the help of another answer.
To achieve that you need to subclass the UICollectionViewFlowLayout (maybe a simple layout also would do but i didn't try).
class HorizontalLineFlowLayout: UICollectionViewFlowLayout {
var insets: CGFloat = 10.0
static let lineWidth: CGFloat = 10.0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var attributesCopy: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
attributesCopy += [attribute]
let indexPath = attribute.indexPath
if collectionView!.numberOfItems(inSection: indexPath.section) == 0 { continue }
let contains = attributes.contains { layoutAttribute in
layoutAttribute.indexPath == indexPath && layoutAttribute.representedElementKind == HorizontalLineDecorationView.kind
}
if !contains {
let horizontalAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HorizontalLineDecorationView.kind, with: indexPath)
let width = indexPath.item == collectionView!.numberOfItems(inSection: indexPath.section) - 1 ?
attribute.frame.width + insets :
attribute.frame.width + insets * 1.5
let frame = CGRect(x: attribute.frame.minX, y: attribute.frame.minY, width: width, height: 0.5)
horizontalAttribute.frame = frame
attributesCopy += [horizontalAttribute]
}
}
return attributesCopy
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
the func layoutAttributesForElements is called each time, and it makes the lines for the cells you specify and for the frames you specify.
you would also need the decoration view which looks like that:
class HorizontalLineDecorationView: UICollectionReusableView {
static let kind = "HorizontalLineDecorationView"
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .gray
alpha = 0.2
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
in general its just a view that goes behind the cell, respectively to its indexPath, so the lines are not all along the screen but those are some lines gathered togather which look like a full line, you can adjust its width and height, play with that.
notice that frame that is set for an attribute in the layout, that is the frame of the decoration view, and is defined with respect to the cell (attribute).
dont forget to register that decoration view and also to make the layout and pass it to the collecitonview like this:
let layout = HorizontalLineFlowLayout()
layout.register(HorizontalLineDecorationView.self, forDecorationViewOfKind: HorizontalLineDecorationView.kind)
layout.minimumInteritemSpacing = 10.0
layout.minimumLineSpacing = 10.0
layout.sectionInset = .zero
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(DateCell.self, forCellWithReuseIdentifier: "month")
collectionView.delegate = monthDelegate
collectionView.dataSource = monthDelegate
collectionView.backgroundColor = .clear
the end result is

UITableView with autolayout not smooth when scrolling

I'm using XIB files to design cells in my UITableView. I also use the dequeue mecanism, for instance : let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell. I precalculate all my row height in the viewDidLoad of my ViewController so the method func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat returns instantly the right value. All of this works.
In my UITableViewCell, I'm using many dynamic height labels (lines = 0). The layout is like this :
I don't use transparent background, all my subviews are opaque with a specified background color. I checked with the Color Blended Layers (everything is green) and Color Misaligned Images (nothing is yellow).
Here is my cellForRowAtIndexPath method :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let tableViewSection = tableViewSectionAtIndex(indexPath.section)
let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow
switch tableViewRow.type! {
case TableViewRowType.Article :
let article = tableViewRow.article!
if article.type == TypeArticle.Article {
let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("chroniqueCell", forIndexPath: indexPath) as! ChroniqueTableViewCell
return cell
}
default:
return UITableViewCell()
}
}
And then, in the willDisplayCell method :
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let tableViewSection = tableViewSectionAtIndex(indexPath.section)
let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow
let article = tableViewRow.article!
if cell.isKindOfClass(ArticleTableViewCell) {
let cell = cell as! ArticleTableViewCell
cell.delegate = self
cell.article = article
if let imageView = articleImageCache[article.id] {
cell.articleImage.image = imageView
cell.shareControl.image = imageView
} else {
loadArticleImage(article, articleCell: cell)
}
} else {
let cell = cell as! ChroniqueTableViewCell
cell.delegate = self
cell.article = article
if let chroniqueur = article.getChroniqueur() {
if let imageView = chroniqueurImageCache[chroniqueur.id] {
cell.chroniqueurImage.image = imageView
} else {
loadChroniqueurImage(article, articleCell: cell)
}
}
}
}
All my images are downloaded in a background thread, so there is no image loading when scrolling.
The layout is modified in my ArticleTableViewCell when I set the "article" property : cell.article = article :
var article: Article? {
didSet {
updateUI()
}
}
And my updateUI function :
func updateUI() -> Void {
if let article = article {
if let surtitre = article.surtitre {
self.surtitre.text = surtitre.uppercaseString
self.surtitre.setLineHeight(3)
} else {
self.surtitre.hidden = true
}
self.titre.text = article.titre
self.titre.setLineHeight(3)
if let amorce = article.amorce {
self.amorce.text = amorce
self.amorce.setLineHeight(3)
} else {
self.amorce.hidden = true
}
if let section = article.sectionSource {
if section.couleurFoncee != "" {
self.bordureSection.backgroundColor = UIColor(hexString: section.couleurFoncee)
self.surtitre.textColor = UIColor(hexString: section.couleurFoncee)
}
}
}
}
The problem is when setting the label text, this causes the lag. The setLineHeight method transforms the text of the label into an NSAttributedString to specify the line height, but even when removing this code and simply setting the text label, a small lag occurs when displaying a new cell.
If I remove all the label setup code, the cells displays the default label text and the tableview scroll is perfectly smooth, and the heights are correct too. Whenever I set a label text, the lag occurs.
I'm running the app on my iPhone 6s. On the 6s simulator, there's absolutely no lag at all, perfectly smooth.
Any idead? Maybe it's because I'm using a UIStackView to embed my labels? I did it so because it's easier to hide labels when empty, so the other elements are moved up, to avoid having spacing where the empty label is.
I tried many thing but can't get the tableview to scroll smoothly, any help would be appreciated.
All my optimizations made it almost 100% fluid on a 6s device but on a 5s, it's really not smooth. I even don't wanna test on a 4s device! I reached the autolayout performance limit when using multiple multiline labels.
After a deep analysis of the Time Profiler, the result is that dynamic labels height (3 in my case) with constraint between them, and those label have attributed text (used to set the line height, but this is not the bottleneck), it seems like the lag is caused by the UIView::layoutSubviews which renders the labels, update the constraints, etc... This is why what when I don't change the label text, everything is smooth. The only solution here is not to use autolayout and layout de subviews programmatically in layoutSubviews method of a custom UITableViewCell subclass.
For those wondering how to do this, I achieved to make a 100% smooth scroll without autolayout and multiple lables with dynamic height (multiline). Here is my UITableView subclass (I use a base class because I have 2 similar cell types) :
//
// ArticleTableViewCell.swift
//
import UIKit
class ArticleTableViewCell: BaseArticleTableViewCell {
var articleImage = UIImageView()
var surtitre = UILabel()
var titre = UILabel()
var amorce = UILabel()
var bordureTop = UIView()
var bordureLeft = UIView()
var articleImageWidth = CGFloat(0)
var articleImageHeight = CGFloat(0)
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.articleImage.clipsToBounds = true
self.articleImage.contentMode = UIViewContentMode.ScaleAspectFill
self.bordureTop.backgroundColor = UIColor(colorLiteralRed: 219/255, green: 219/255, blue: 219/255, alpha: 1.0)
self.bordureLeft.backgroundColor = UIColor.blackColor()
self.surtitre.numberOfLines = 0
self.surtitre.font = UIFont(name: "Graphik-Bold", size: 11)
self.surtitre.textColor = UIColor.blackColor()
self.surtitre.backgroundColor = self.contentView.backgroundColor
self.titre.numberOfLines = 0
self.titre.font = UIFont(name: "PublicoHeadline-Extrabold", size: 22)
self.titre.textColor = UIColor(colorLiteralRed: 26/255, green: 26/255, blue: 26/255, alpha: 1.0)
self.titre.backgroundColor = self.contentView.backgroundColor
self.amorce.numberOfLines = 0
self.amorce.font = UIFont(name: "Graphik-Regular", size: 12)
self.amorce.textColor = UIColor.blackColor()
self.amorce.backgroundColor = self.contentView.backgroundColor
self.contentView.addSubview(articleImage)
self.contentView.addSubview(surtitre)
self.contentView.addSubview(titre)
self.contentView.addSubview(amorce)
self.contentView.addSubview(bordureTop)
self.contentView.addSubview(bordureLeft)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let article = article {
var currentY = CGFloat(0)
let labelX = CGFloat(18)
let labelWidth = fullWidth - 48
// Taille de l'image avec un ratio de 372/243
articleImageWidth = ceil(fullWidth - 3)
articleImageHeight = ceil((articleImageWidth * 243) / 372)
self.bordureTop.frame = CGRect(x: 3, y: 0, width: fullWidth - 3, height: 1)
// Image
if article.imagePrincipale == nil {
self.articleImage.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
self.bordureTop.hidden = false
} else {
self.articleImage.frame = CGRect(x: 3, y: 0, width: self.articleImageWidth, height: self.articleImageHeight)
self.bordureTop.hidden = true
currentY += self.articleImageHeight
}
// Padding top
currentY += 15
// Surtitre
if let surtitre = article.surtitre {
self.surtitre.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
self.surtitre.preferredMaxLayoutWidth = self.surtitre.frame.width
self.surtitre.setTextWithLineHeight(surtitre.uppercaseString, lineHeight: 3)
self.surtitre.sizeToFit()
currentY += self.surtitre.frame.height
currentY += 15
} else {
self.surtitre.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
}
// Titre
self.titre.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
self.titre.preferredMaxLayoutWidth = self.titre.frame.width
self.titre.setTextWithLineHeight(article.titre, lineHeight: 3)
self.titre.sizeToFit()
currentY += self.titre.frame.height
// Amorce
if let amorce = article.amorce {
currentY += 15
self.amorce.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
self.amorce.preferredMaxLayoutWidth = self.amorce.frame.width
self.amorce.setTextWithLineHeight(amorce, lineHeight: 3)
self.amorce.sizeToFit()
currentY += self.amorce.frame.height
} else {
self.amorce.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
}
// Boutons
currentY += 9
self.updateButtonsPosition(currentY)
self.layoutUpdatedAt(currentY)
currentY += self.favorisButton.frame.height
// Padding bottom
currentY += 15
// Couleurs
self.bordureLeft.frame = CGRect(x: 0, y: 0, width: 3, height: currentY - 2)
if let section = article.sectionSource {
if let couleurFoncee = section.couleurFoncee {
self.bordureLeft.backgroundColor = couleurFoncee
self.surtitre.textColor = couleurFoncee
}
}
// Mettre à jour le frame du contentView avec la bonne hauteur totale
var frame = self.contentView.frame
frame.size.height = currentY
self.contentView.frame = frame
}
}
}
And the base class :
//
// BaseArticleTableViewCell.swift
//
import UIKit
class BaseArticleTableViewCell: UITableViewCell {
var backgroundThread: NSURLSessionDataTask?
var delegate: SectionViewController?
var favorisButton: FavorisButton!
var shareButton: ShareButton!
var updatedAt: UILabel!
var fullWidth = CGFloat(0)
var article: Article? {
didSet {
// Update du UI quand on set l'article
updateArticle()
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = UITableViewCellSelectionStyle.None
self.contentView.backgroundColor = UIColor(colorLiteralRed: 248/255, green: 248/255, blue: 248/255, alpha: 1.0)
// Largeur de la cellule, qui est toujours plein écran dans notre cas
// self.contentView.frame.width ne donne pas la bonne valeur tant que le tableView n'a pas été layouté
fullWidth = UIScreen.mainScreen().bounds.width
self.favorisButton = FavorisButton(frame: CGRect(x: fullWidth - 40, y: 0, width: 28, height: 30))
self.shareButton = ShareButton(frame: CGRect(x: fullWidth - 73, y: 0, width: 28, height: 30))
self.updatedAt = UILabel(frame: CGRect(x: 18, y: 0, width: 0, height: 0))
self.updatedAt.font = UIFont(name: "Graphik-Regular", size: 10)
self.updatedAt.textColor = UIColor(colorLiteralRed: 138/255, green: 138/255, blue: 138/255, alpha: 1.0)
self.updatedAt.backgroundColor = self.contentView.backgroundColor
self.addSubview(self.favorisButton)
self.addSubview(self.shareButton)
self.addSubview(self.updatedAt)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Avant qu'une cell soit réutilisée, faire un cleanup
override func prepareForReuse() {
super.prepareForReuse()
// Canceller un background thread si y'en a un actif
if let backgroundThread = self.backgroundThread {
backgroundThread.cancel()
self.backgroundThread = nil
}
resetUI()
}
// Updater le UI
func updateArticle() {
self.favorisButton.article = article
self.shareButton.article = article
if let delegate = self.delegate {
self.shareButton.delegate = delegate
}
}
// Faire un reset du UI avant de réutiliser une instance de Cell
func resetUI() {
}
// Mettre à jour la position des boutons
func updateButtonsPosition(currentY: CGFloat) {
// Déjà positionnés en X, width, height, reste le Y
var shareFrame = self.shareButton.frame
shareFrame.origin.y = currentY
self.shareButton.frame = shareFrame
var favorisFrame = self.favorisButton.frame
favorisFrame.origin.y = currentY + 1
self.favorisButton.frame = favorisFrame
}
// Mettre à jour la position du updatedAt et son texte
func layoutUpdatedAt(currentY: CGFloat) {
var frame = self.updatedAt.frame
frame.origin.y = currentY + 15
self.updatedAt.frame = frame
if let updatedAt = article?.updatedAtListe {
self.updatedAt.text = updatedAt
} else {
self.updatedAt.text = ""
}
self.updatedAt.sizeToFit()
}
}
On the viewDidLoad of my ViewController, i pre-calculate all the row height :
// Créer une cache des row height des articles
func calculRowHeight() {
self.articleRowHeights = [Int: CGFloat]()
// Utiliser une seule instance de chaque type de cell
let articleCell = tableView.dequeueReusableCellWithIdentifier("articleCell") as! BaseArticleTableViewCell
let chroniqueCell = tableView.dequeueReusableCellWithIdentifier("chroniqueCell") as! BaseArticleTableViewCell
var cell: BaseArticleTableViewCell!
for articleObj in section.articles {
let article = articleObj as! Article
// Utiliser le bon type de cell
if article.type == TypeArticle.Article {
cell = articleCell
} else {
cell = chroniqueCell
}
// Setter l'article et refaire le layout
cell.article = article
cell.layoutSubviews()
// Prendre la hauteur générée
self.articleRowHeights[article.id] = cell.contentView.frame.height
}
}
Set the row height for the requested cell :
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let tableViewSection = tableViewSectionAtIndex(indexPath.section)
let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow
switch tableViewRow.type! {
case TableViewRowType.Article :
let article = tableViewRow.article!
return self.articleRowHeights[article.id]!
default:
return UITableViewAutomaticDimension
}
}
Return the cell in cellForRowAtIndexPath (I have multiple cell types in my tableView, so there's a couple of checks to do) :
// Cellule pour un section/row spécifique
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let tableViewSection = tableViewSectionAtIndex(indexPath.section)
let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow
switch tableViewRow.type! {
case TableViewRowType.Article :
let article = tableViewRow.article!
if article.type == TypeArticle.Article {
let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell
cell.delegate = self
cell.article = article
if let imageView = articleImageCache[article.id] {
cell.articleImage.image = imageView
cell.shareButton.image = imageView
} else {
cell.articleImage.image = placeholder
loadArticleImage(article, articleCell: cell)
}
return cell
}
return UITableViewCell()
default:
return UITableViewCell()
}
}

UICollectionViewCell UIImage Lag even with dispatch_async

Symptom lag appears upon rendering a cell in collection view cell, I've recorded it here https://youtu.be/2utKOpHMshs
I'm not quite sure how the lag is happening and starting to suspect it has something to do with the recipe name element else rather than the UIImage since it's on another thread.
Async function
func asyncLoadImage(data: NSData, imageView: UIImageView) {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
var image: UIImage!
NSLog("loading image")
image = UIImage(data: data)
dispatch_async(dispatch_get_main_queue()) {
imageView.image = image
NSLog("render image")
}
}
}
RecipeListViewController
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Storyboard.CellIdentifier, forIndexPath: indexPath) as!RecipeListCollectionViewCell
let recipe = recipes[indexPath.item] as! Recipe
cell.recipeName?.text = recipe.name?.uppercaseString
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.mainScreen().scale
if (recipe.photo != nil) {
asyncLoadImage(recipe.photo!, imageView: cell.recipeImageView)
}
return cell
}
UICollectionViewCell:init
recipeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height-100))
recipeImageView.contentMode = UIViewContentMode.ScaleAspectFill
recipeImageView.clipsToBounds = true
self.addSubview(recipeImageView)
recipeName = UILabel(frame: CGRect(x: 0, y: frame.size.height-100, width: frame.size.width, height: 50))
recipeName.backgroundColor = UIColor(red: 255, green: 255, blue: 255, alpha: 1)
recipeName.textColor = UIColor.blackColor()
recipeName.textAlignment = NSTextAlignment.Center
recipeName.font = UIFont.boldSystemFontOfSize(18.00)
self.addSubview(recipeName)
The issue resolved by removing the following code
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.mainScreen().scale

Swift - tableViewCell issue when scrolling

I'm creating a chat app using swift. On my storyboard, I have one View Controller that is responsible for showing the messages. I have 3 swift files :
ChatingViewController : Class associated with the View Controller on the storyboard
CustomChatingTableViewController
CellChatingTableViewCell
Each message is displayed in a cell. I create the tableView programmatically.
- ChatingViewController
import UIKit
class ChatingViewController: UIViewController {
var messageController = [[String:String]]()
var tableViewController = CustomChatingTableViewController()
override func viewDidLoad() {
let sizeTableView = CGSize(width: view.frame.size.width - 2 * margin, height: view.frame.size.height - sizeTextField.height - 2 * margin - self.navigationController!.navigationBar.frame.size.height)
let originTableView = CGPoint(x: margin, y: 2 * margin + self.navigationController!.navigationBar.frame.size.height)
tableViewController.tableView.frame = CGRect(origin: originTableView, size: sizeTableView)
tableViewController.tableView.registerClass(CellChatingTableViewCell.self, forCellReuseIdentifier: "Cell")
tableViewController.data = messageController
tableViewController.tableView.separatorStyle = .None
view.addSubview(tableViewController.tableView)
}
- CustomChatingTableViewController
import UIKit
class CustomChatingTableViewController: UITableViewController {
var data:[[String:String]]!
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as CellChatingTableViewCell
cell.setCell(data[indexPath.row]["name"] as String!, date: data[indexPath.row]["date"] as String!, message: data[indexPath.row]["message"] as String!)
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 150
}
}
** -CellChatingTableViewCell**
import UIKit
class CellChatingTableViewCell: UITableViewCell {
var date = UILabel()
var message = UILabel()
func setCell(name:String,date:String,message:String){
let imageContainerMessage = UIImage(named: "orange.png")!.stretchableImageWithLeftCapWidth(24, topCapHeight: 15)
self.date.font = UIFont(name: "Arial", size: 10)
self.date.text = date
self.date.numberOfLines = 0
self.date.lineBreakMode = NSLineBreakMode.ByWordWrapping
let sizeDateLabelMax = CGSizeMake(self.frame.size.width, 9999)
let expectedSizeDate = self.date.sizeThatFits(sizeDateLabelMax)
self.date.frame = CGRect(origin: CGPoint.zeroPoint, size: expectedSizeDate)
self.message.font = UIFont(name: "Arial", size: 15)
self.message.text = message
self.message.numberOfLines = 0
self.message.lineBreakMode = NSLineBreakMode.ByWordWrapping
let sizeMessageLabelMax = CGSizeMake(self.frame.size.width, 9999)
let expectedSizeMessage = self.message.sizeThatFits(sizeMessageLabelMax)
self.message.frame = CGRect(origin: CGPoint(x: 15, y: 10), size: expectedSizeMessage)
var imageContainer = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: expectedSizeDate.height + 5), size:
CGSizeMake(expectedSizeMessage.width + 25, expectedSizeMessage.height + 25)))
imageContainer.image = imageContainerMessage
self.addSubview(self.date)
self.addSubview(imageContainer)
imageContainer.addSubview(self.message)
}
}
When I load the ViewController, everything does work fine but when I scroll the tableView, it turns horrible:
Before scrolling :
After scrolling :
Any suggestion?
Thanks in advance
In your cellForRowAtIndexPath, you are reusing cells. But then in your custom UITableViewCell, you are adding (self.addSubView) self.date and self.message multiple times (yikes), and adding new instances of imageContainer.
You should either clear the cells before re-adding them, or invalidate them with new data but do not do self.addSubView again.

Resources