UITableView called many times when the cell is out of the screen - ios

I have a problem when i use the function tableView. I show you my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cardCell", for: indexPath) as! OpsCardCell
cell.workTest(drawCard: .selectOpponent)
return cell
}
In the exemple I have 4 cell and when I scroll in the simulator the cell who are out of the screen and come back, the cell is again called. And since I draw the card dynamically, the card was drawn several times and the shadow I adds too many times. I show you the screen before and after:
after few scroll down and scroll up:
this is because the function tableView called many times the cell [0] and [3]
This is my code to draw the card:
func drawBasiqCard(){
let cardView = UIView()
self.addSubview(cardView)
cardView.frame = CGRect(marginCardWidth,
marginCardHeight,
self.bounds.size.width - (marginCardWidth*2),
self.bounds.size.height - (marginCardHeight*2))
cardView.layer.cornerRadius = 10
let rounding = CGFloat.init(10)
let shadowLayer = CAShapeLayer.init()
shadowLayer.path = UIBezierPath.init(roundedRect: cardView.bounds, cornerRadius: rounding).cgPath
shadowLayer.fillColor = UIColor(rgb: 0xaaccbb).cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowRadius = 5
shadowLayer.shadowOpacity = 0.2
shadowLayer.shadowOffset = CGSize.init(width: 0, height: 0)
cardView.layer.insertSublayer(shadowLayer, at: 0)
}
So my question is, what is wrong with my code? And there is another way to solve my problem ?
thanks to your reply !

You should try to add sublayer at once in your awakefromnib method. Tableviewcell reuse the same cell using cell identifier that why multiple shadows added to your cell.

Your problem is cell recycling. When you scroll, the same cell gets re-used to display data at a different indexPath.
You should create a custom subclass of UITableViewCell. Give it an optional property shadowLayer. In your cellForRow(at:) method, dequeue a cell and cast it to the correct custom class. Then check to see if its shadowLayer property is nil. If it is nil, add a shadow layer. If it's not nil, create a shadow layer and install it in the cell (cell.shadowLayer = shadowLayer).

In you code you have taken new card each time that card drawing method called, so this will work fine for the first time, but after that this will create problem because you haven't removed that view or you have not checked is that card is already added in cell or not.
So you can either remove below line from you code and take one parent view to design your card inside your storyboard or xib, if you are going to design your cell using xib or storyboard.
let cardView = UIView()
Or if you are going to design your cell programatically then first check if that cardview is added in cell or not, and then cardview is not added then add new one else skip that code.

So I add to the top of my class :
private var ShadowLayerCard: CAShapeLayer?
and I compare if the ShadowLayerCard is not nil :
if(self.ShadowLayerCard == nil){
let shadowLayer = CAShapeLayer.init()
self.ShadowLayerCard = shadowLayer
// here I have my code to add the shadowLayer and other parameters of the shadow...
}
this solve my problem, thanks to duncan-c

Related

How to add views/layers dynamically in a horizontal collectionView by indexPath

Im making a timeline of sorts with a horizontal collectionView where for each block of time (a month), I have a cell with UIViews for each day with different colors. Some months have 30 views added, some 31, some 28. I am having trouble adding views dynamically to each cell such that they are not duplicated or added to the wrong cell.
To that end, I have created a simplified version of this timeline where each month only has 1 layer/view added to it dynamically - I will then try and tackle adding variable amounts of views/layers.
I made this project as the simplest possible example of what I'm talking about:
https://github.com/AlexMarshall12/testTimeline-iOS
Here is the code in ViewController.swift:
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var filledCells: [Int] = []
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.filledCells = [1,28]
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 28
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
print(indexPath.item)
cell.myLabel.text = String(indexPath.item)
if filledCells.contains(indexPath.item) {
let tickLayer = CAShapeLayer()
tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
tickLayer.fillColor = UIColor(red:0.99, green:0.13, blue:0.25, alpha:0.5).cgColor
cell.layer.addSublayer(tickLayer)
} else {
//updated
let tickLayer = CAShapeLayer()
tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
tickLayer.fillColor = UIColor(red:0.1, green:0.13, blue:0.98, alpha:0.5).cgColor
cell.layer.addSublayer(tickLayer)
}
return cell
}
}
The idea is that for each indexPath item (each cell?) it looks to see if that is contained in the self.filledCells array: 1 or 28 which happen to be the outer edges as there are 28 cells returned for numberOfItemsInSection and 1 section. So what I wanted to happen was for every cell to be light blue except for the 1rst and 28th - light red.
However, as you can see here https://imgur.com/a/KTLn7Cb. There are multiple shades of blue and red and purple as cells are filled multiple times as I scroll back and forth, certainly ones other than 1 and 28.
I think there are two issues.
Somehow indexPath.item is returning 1 or 28 even when I'm not scrolled to the very edge cells. Why is this?
When I revisit cells that are already rendered, it re-renders them. I'm not sure why this is. I was wondering if overriding prepareForReuse() could help but I've heard this is often a bad idea so Im not sure if its what I'm looking for.
Any advice on achieving this?
You're overlooking the fact that cells are reused. When you change a cell fillColor, and it gets scrolled off, the cell that gets scrolled on reuses that cell, and you've just set its fill color to red, and you didn't turn it off. Set the fillColor explicitly for each cell, whether it's red, white or clear color, don't set it for just the selected cells.
Its always a bad idea to add a view or a layer every time in table view cells or collection view cells. As you are reusing cells, there is no need to create tickLayer again and again. Make tickLayer as a global variable, initialize it in awakeFromNib funciton in MyCollectionViewCell, then change its fill color in cellForRow function.
Adding/removing views to a cell is not a good idea.
There are two ways to finish this:
① Create different cells by their subviews;
② Create a generic cell and control the subview‘s display via hidden and layout, and update when you get this cell with the new data.
Adding on to what #Owen Hartnett says about reusability of cells, I would like to point out that you are recreating layers each time! This is equivalent of creating subviews.
Every time you encounter a cell inside cellForItemAt function, you should also check existence of pre-added layers. Something like (not syntactically perfect Swift, but pseudocode):
let bLayerFound = false;
for (layer: CALayer in cell.layer.sublayers)
{
if (layer.name.equals("mylayer"))
{
let shapeLayer = layer as! CAShapeLayer
bLayerFound = true;
//set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
shapeLayer.path = //path
shapeLayer.fillColor = //color
}
}
if (bLayerFound == false)
{
//create layer here, again
let tickLayer = CAShapeLayer()
//set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
tickLayer.path = //path
tickLayer.fillColor = //color
tickLayer.name = "mylayer"
cell.layer.addSublayer(tickLayer)
}
Couple of other caveats:
It is better always to add UI to cell.contentView, not cell itself.
Once you get it running, consider moving this code off to your UICollectionViewCell subclass. If there is no subclass, maybe a separate function, so as not to clutter cellForItemAt with nasty if-else logic.

UI CollectionView in UICollectionView Cell Programmatically

Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.

Images in UIImageView not showing in UITableView when circular mask is applied to the ImageView unless scroll

Thanks in advance for the help.
I have a UITableView within a main view contoller. Within the prototype cell, I have a UIImageview. In the code below everything works until I add the 5 lines to apply a circular mask and border. Once I do that, the images will not load unless I scroll the cells. The mask and border do get applied perfectly however. Will be great when it works... but until then.
Certainly this has been seen before. I'm a swift/objective-C newbie.
Working in swift for this one.
Code below;
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("mixerCell", forIndexPath: indexPath) as! MixerTableViewCell
// set label background color to clear
cell.textLabel?.backgroundColor = UIColor.clearColor()
// set highlight selection to none
cell.selectionStyle = .None
// set image for cell
let imageView = cell.viewWithTag(1) as! UIImageView
// put circular mask and border. This is the problem code that causes initial load of the tableview images to show up blank.
imageView.layer.cornerRadius = imageView.frame.size.width / 2;
imageView.clipsToBounds = true;
let color1 = UIColor(white: 1.0, alpha: 0.5).CGColor as CGColorRef
imageView.layer.borderWidth = 2;
imageView.layer.borderColor = color1
// assign image
imageView.image = UIImage(named: mixerSounds[indexPath.row])
return cell
}
initial view load
after scroll
your code is perfectly working for me. Here i am using Xcode-7. i think you are using Xcode-6.3 or less version. just upgrade it to Xcode- 7. and if you are using the same then just check your heightforRowAtIndexpath or other delegates there should be some issue.
thanks
Try changing the below lines,
// replace this
let imageView = cell.viewWithTag(1) as! UIImageView
// to
let imageView = cell.yourImageViewName
/* yourImageViewName is the outlet
reference name you have given in the
MixerTableViewCell custom class.
*/
Edit 2: Just for debugging purposes,
hardcode the image name and check if the image appears on the all the cells.
imageView.image = UIImage(named: "first1.png")
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
{
let cellIdentifier = "cell"
var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as UITableViewCell
cell.image_View.image = UIImage(named: mixerSounds[indexPath.row])
println("The loaded image: \(image)")
cell.image_View.layer.masksToBounds = false
cell.image_View.layer.borderColor = UIColor.blackColor().CGColor
cell.image_View.layer.cornerRadius = image.frame.height/2
cell.image_View.clipsToBounds = true
return cell
}
Give imageview outlet to cell and not give imageview name because by default name is imageview so take diffrent name
It looks like the problem is using clipToBounds = true I am facing the same issue while making circular UIImageView inside UITableViewCell
I didn't find the exact solution but for now I found a way to do this
if (indexPath.row == indexPath.length && !isTableReloaded)
{
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.000000001 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.reloadTableView()
})
}
func reloadTableView()
{
isTableReloaded = true
self.tableViewContacts.reloadData()
}
Here isTableReloaded is a Bool type var which is initialized to false in viewDidLoad()
and the if condition is to be placed at the last of cellForRowAtIndexPath but before return statement
This will resolve our problem but do not rely on this as this is not the best approach.
Please post solution for this if any one found the better approach.
Here is a perfect and state away solution for circular image in UITableview Cell.
Simply modify your UITableviewCell (custom cell) class with below code.
override func awakeFromNib() {
super.awakeFromNib()
imgEvent.layer.frame = (imgEvent.layer.frame).insetBy(dx: 0, dy: 0)
imgEvent.layer.borderColor = UIColor.gray.cgColor
imgEvent.layer.cornerRadius = (imgEvent.frame.height)/2
imgEvent.layer.masksToBounds = false
imgEvent.clipsToBounds = true
imgEvent.layer.borderWidth = 0.5
imgEvent.contentMode = UIViewContentMode.scaleAspectFill
}
It will also helps to solve the problem of image circular only after scrolling table..(if any!)
let width = cell.frame.size.width
cell.img.layer.cornerRadius = width * 0.72 / 2
0.72 is the ratio of the cell width to image width, for eg. cellWidth = 125 and imageWidth = 90, so 125/90 would give 0.72. Do similar calculation for your image.
First: Images doesn't load until you scroll, because when cellForRowAtIndexPath methods called the constraints doesn't set for image until now, so when scrolling the constraints was added and the image appears, so if you set proportional width and height for imageView (width==height) in cell then
do that
let w = tableview.frame.width*(proportional value like 0.2)
imageView.layer.cornerRadius = w / 2
imageView.clipsToBounds = true;

How to load content view inside a tableview cell?

I have created a table view as
var tblView : UITableView = UITableView()
tblView.frame = CGRectMake(0, 168, 320-50 , 450)
tblView.separatorColor = UIColor.clearColor()
tblView.scrollEnabled = false
tblView.rowHeight = 39
self.addSubview(tblView)
tblView.delegate = self
tblView.dataSource = self
tblView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "myCell")
Now i am trying to add custom view to the tableviewcell as
//MARK: table view data source methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("myCell") as UITableViewCell
var cellImgView:UIImageView = UIImageView()
cellImgView.frame = CGRectMake(cell.contentView.frame.origin.x+10, cell.contentView.frame.size.height, 20, 20)
let cellImage = UIImage(named: self.cellImgs[indexPath.row])
cell.backgroundColor = UIColor(red: 0.000, green: 0.400, blue: 0.404, alpha: 1.00)
cellImgView = UIImageView(image: cellImage)
cell.contentView.addSubview(cellImgView)
return cell
}
Only first image is at correct position and other are overlapped.Why is this happening?
cellForRowAtIndexPath will called each time when cell in user's sight.
But you need add UIImageView only once. So, you need to check, is there is the first call.
Also, it is good practice to use custom UITableViewCell
Tableview is reusing created cells, so your approach is not the best. What you are doing now is creating UIImageView everytime a cell is being used and if its a cells that is going to be reused you will create new UIImageView on top of your previous one.
I would suggest to do all this in interface builder. If you dont want to, at least subclass UITableViewCell and create image view in init method of your cell, that will take care of overlapping UIImageviews over each other, but still consider using interface builder, its the easiest way to build tableviews.
EDIT
Here's some tutorial on tableviews in interface builder, this should help you get started: http://shrikar.com/blog/2015/01/17/uitableview-and-uitableviewcell-customization-in-swift/
Good luck!

reusing a cell in table view in the wrong way? my rows get repeated values when scrolling on my UITableView

I'm trying to implement a table by code, so far so good but when I scroll down and up in my table my rows went crazy, I did a little research and I think its because of the way I reuse my cells, but the examples I found were all in Obj- C, So could you please help me to understand the problem? Here are my function where I implement the cells:
override func tableView(tableView: (UITableView!), cellForRowAtIndexPath indexPath: (NSIndexPath!)) -> UITableViewCell{
let sectionA = seccionesDiccionario[indexPath.section]
let sectionName = tableDataSwiftDictionary[sectionA]!
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("CellId") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "CellId")
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame:CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.orangeColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
cell!.textLabel?.text = sectionName[indexPath.row]
cell!.textLabel?.numberOfLines = 0
cell!.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
cell!.textLabel?.sizeToFit()
}
return cell!
}
Thank you so much.
when I scroll down and up in my table my rows went crazy
what do you mean by 'went crazy'.
One thing that I can see in the above code: You should move the text assignment out of the if statement. You want the 'textLabel' to show the String in the 'sectionName' array at the given indexPath.row. Currently, you are creating some cells and then - when you start scrolling your tableView - the cells are reused but the textLabel's text is not set, so it will always show its initial value.
Move this line
cell!.textLabel?.text = sectionName[indexPath.row]
out of the if{} block. Maybe that's all you need to do here.
EDIT
btw: since you're calling sizeToFit on the textLabel, I assume you want the cell to be high enough to display all the text. Note that sizeToFit will not be enough in order to achieve that. You'll have to either use Auto-sizing cells using AutoLayout (> iOS8) or implement the tableView:heightForRowAtIndexPath: delegate method and return the calculated cell height there.

Resources