Aligning multiple runtime generated UILabels in a UITableView - ios

I have a UITableView that needs to support content by listing style something like
But the tricky part is that the amount of "Label" will vary with each cell, some may have only 4 but also up to 12. Also the "Value" can be either a single word or short phrase up to two lines(like in the image above). So I decided to use UIStackView to help me pack and size my UILabels used to display this. I am currently stuck at a problem when the "Label" varies in length like:
I need the leading end of the "Values" to be aligned like the first image even though the "Label" vary in length. Is there a behaviour of UIStackView that allows so? Or is there another approach that can allow me to obtain the results I need?
Note: Each "Label" and "Value" is in one UIStackView, I did it to align them.
I tried using String Formatting too, but "Values" with more than one line will wrap under the label instead of wrapping by itself like I manage to do in the images.
I tried placing all "Labels" in one UIStackView and all "Values" in another, I could no get them to align like they do in the images once the "Value" is more than one line.
Or if it might be a mistake I made somewhere, this is how I created the UIStackViews:
var higherCount = 0
if labels.count<values.count {
higherCount = values.count
} else {
higherCount = labels.count
}
mainStackView = UIStackView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
for i in 0..<higherCount {
var height:CGFloat = 20
if (values[i] as NSString).size(attributes: [NSFontAttributeName:UIFont.systemFont(ofSize: UIFont.systemFontSize)]).width > frame.width {
height = 42
}
let stackViewToAdd = UIStackView(frame: CGRect(x: 0, y: 0, width: frame.width, height: height))
let label = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: height))
if labels.count<higherCount {
label.text = ""
} else {
label.text = labels[i]
}
label.setContentCompressionResistancePriority(1000, for: .horizontal)
label.setContentHuggingPriority(999, for: .horizontal)
stackViewToAdd.addArrangedSubview(label)
let value = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: height))
if values.count<higherCount {
value.text = ""
} else {
value.text = values[i]
}
value.lineBreakMode = .byWordWrapping
value.numberOfLines = 2
stackViewToAdd.addArrangedSubview(value)
mainStackView?.addArrangedSubview(stackViewToAdd)
}
mainStackView?.alignment = .fill
mainStackView?.axis = .vertical
mainStackView?.distribution = .fill

I you created your cell programmatically, then you can resize the cell programmatically depends on the size of UILabel Content.
In my case Label font is UIFont.systemFont(ofSize: 15), minimum TableViewCell height is 50, arrLabel1 and arrLabel2 will be the content of Labels.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrLable1.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "\(indexPath.row)")
let lable1 = UILabel(frame: CGRect(x: 10, y: 10, width: view.frame.size.width - 20, height: 0))
lable1.numberOfLines = 0
lable1.font = UIFont.systemFont(ofSize: 15)
lable1.text = arrLable1[indexPath.row]
lable1.sizeToFit()
cell.addSubview(lable1)
let lable2 = UILabel(frame: CGRect(x: 10, y: lable1.frame.origin.y + lable1.frame.size.height + 10 , width: view.frame.size.width - 20, height: 0))
lable2.numberOfLines = 0
lable2.font = UIFont.systemFont(ofSize: 15)
lable2.text = arrLable2[indexPath.row]
lable2.sizeToFit()
cell.addSubview(lable2)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath)
-> CGFloat {
let boundingRect1 = arrLable1[indexPath.row].boundingRect(with: CGSize(width: view.frame.size.width - 40 , height: CGFloat(MAXFLOAT)), options: NSStringDrawingOptions.usesLineFragmentOrigin,attributes:[ NSFontAttributeName : UIFont.systemFont(ofSize: 15)] ,context: nil)
let boundingRect2 = arrLable2[indexPath.row].boundingRect(with: CGSize(width: view.frame.size.width - 40 , height: CGFloat(MAXFLOAT)), options: NSStringDrawingOptions.usesLineFragmentOrigin,attributes:[ NSFontAttributeName : UIFont.systemFont(ofSize: 15)] ,context: nil)
guard boundingRect1.height + boundingRect2.height + 30 > 50 else {
return 50
}
return boundingRect1.height + boundingRect2.height + 30
}
Set Label numberOfLines to 0

Related

Cells with Dynamically created Heights overlap

Hi im newer to IOS dev and Im more familiar with web dev. I am running into an issue where my cells are originally rendered how I would expect them to, but after scrolling to the bottom and scrolling back to the top of my screen I noticed that the cells begin to overlap each other. After reading other stack overflow posts I am starting to think that it has to do with how I create the cells. I am attempting to create multiple labels in a cell with a for loop similar to how someone would with PHP for web dev and im wondering if this is the proper way to do it/ is this a possible cause.
for i in 0...bars.count-1 {
var barLabelView:UIView = UIView()
var barLabel:UILabel = UILabel()
barLabelView.frame = CGRect(x: 20, y: CGFloat(20 + (70 * i)), width: barView.frame.width-40, height: 50)
barLabelView.backgroundColor = UIColor(displayP3Red: 27/255.0, green: 99/255.0, blue: 222/255.0, alpha: 1)
barLabelView.layer.cornerRadius = 10
barLabel.frame = CGRect(x: 0, y: 0, width: barLabelView.frame.width-10, height: 50)
barLabel.text = bars[i]
barLabel.textAlignment = .center
barLabel.numberOfLines = 0
barLabel.font = UIFont(name: "Chalkboard SE", size: 15)
barView.addSubview(barLabelView)
barLabelView.addSubview(barLabel)
}
Also heres how i set the height of the cell:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let crawlNameText = self.posts[indexPath.row].text
let size = CGSize(width: view.frame.width - 40, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedCrawlNameFrame = NSString(string: crawlNameText).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], context: nil)
let bars = posts[indexPath.row].bars.components(separatedBy: ", ")
let buds = posts[indexPath.row].buds.components(separatedBy: ", ")
if (self.posts[indexPath.row].type == "crawl") {
var cellHeightP1 = CGFloat(estimatedCrawlNameFrame.height + 110 + 20 + 20 + 20 + 40 + 60)
var cellHeightP2:CGFloat = CGFloat( 80 * bars.count)
var cellHeightP3:CGFloat = CGFloat( 80 * buds.count)
var cellHeight = cellHeightP1 + cellHeightP2 + cellHeightP3
return cellHeight
} else {
return estimatedCrawlNameFrame.width + 110 + 20
}
}
and here is my cellForRowAt function
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = nfTV.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! newsFeedCell
// Im guessing I would set the cell to nill here to clear the buttons I made
cell.setPost(post: posts[indexPath.row])
if (cell.profilePic != nil){
cell.profilePic.layer.cornerRadius = cell.profilePic.bounds.height / 2
cell.profilePic.clipsToBounds = true
}
cell.backgroundColor = UIColor.lightGray
print("made it here")
print(indexPath.row)
cell.frame = CGRect(x: 0, y: 0, width: cell.frame.width, height: cell.frame.height)
return cell
}
I read that I might have to clear my cell before reusing it but im not sure how to set a cell to nil. I am also wondering if using a collection view would work better but I dont know the difference between that and a tableview.
Any help or suggestions is appreciated, thanks.
I answered my own question but I guess Ill leave this up in case it helps anyone in the future
I added the following lines of code at my comment in the cellForRowAt function to remove the buttons
for v in cell.barView.subviews{
v.removeFromSuperview()
}
for v in cell.budsView.subviews{
v.removeFromSuperview()
}
The issue was caused by some cells having more buttons than others so cells with less buttons were rendered with more buttons than necessary making it appear like it was overlap but in reality it was just recycled buttons.

Custom image and switch not showing in collection view controller at right place

My requirement is, I need to populate 10 cell's in collection view controller and I need to display in 6Th cell battery status and in 7th need to switch. The remaining cell is not required to show the battery status and switch. I am adding these two (battery status & switch) through programmatically.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
if indexPath.row == 6 {
let percentage: CGFloat = 0.4
let imageview:UIImageView=UIImageView(frame: CGRect(x: 10, y: 50, width: 30, height: 18))
let img : UIImage = UIImage(named:"batteryIcon")!
imageview.image = img
let batteryView = UIView.init(frame: CGRect(x: 10, y: 50, width: 30, height: 18))
cell.addSubview(batteryView)
let colorRect = CGRect(x: batteryView.bounds.origin.x, y: batteryView.bounds.origin.y, width: batteryView.bounds.width * percentage, height: batteryView.bounds.height)
let coloredView = UIView(frame: colorRect)
coloredView.backgroundColor = UIColor.green
batteryView.addSubview(coloredView)
cell.contentView.addSubview(imageview)
} else if indexPath.row == 7 {
let switchDemo = UISwitch(frame: CGRect(x: 10, y: 40, width: 37, height: 18))
switchDemo.isOn = true
switchDemo.setOn(true, animated: false)
switchDemo.transform = CGAffineTransform(scaleX: 0.70, y: 0.70)
switchDemo.addTarget(self, action: #selector(switchValueDidChange), for: .valueChanged)
cell.contentView.addSubview(switchDemo)
}
return cell
}
Here it is Working fine. But the problem is when I call "collectionView.reloadData()" Instead of showing battery status and switch 6Th and 7Th cells, Those two appearing in other cells also.
I am not getting, What mistake I am done.
For a better understanding of my requirement, I am sharing an image.

iOS - AccessoryView changes the size of custom separator

I have a grouped tableview and at first I wanted to remove the top and bottom separator. I looked around and found a solution that worked great form me. It was by adding a UIView to the bottom of the cell to act as the separator. Now it looks amazing. But the problem I faced is when I set the AccessoryView of the cell , because for some reason when I select the cell , my custom separator change its width . Here's a representation of the problem :
this is when not selected
this is when selected :
Note that the custom separator already has constraint .
Any suggestions please ?
Edit:
Here's how I set the accessoryView :
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let cell = tableView.cellForRow(at: indexPath) as? TableViewCell {
let label = UILabel()
label.text = "x"
label.sizeToFit()
label.font = UIFont.systemFont(ofSize: 13, weight: .medium)
if(cell.accessoryView == nil){
cell.accessoryView = label
}else{
cell.accessoryView = nil
}
}
}
that's it and everything else is just a simple tableview with rows and section , I also set the separator to none in storyboard.
Edit 2 :
This is the separator constraints :
You need to change your trailing constraint. Instead of ContentView add trailing constraint to cell. Remove trailing constraint on separator view and add constraints as in image.
In iOS 13+, I had to change the code to draw bottom border on the cell instead of the cell's contentView. I do it in awakeFromNib in the Cell class.
override func awakeFromNib() {
super.awakeFromNib()
addBorder(borderType: .bottom)
}
func addBorder(borderType: BorderType, width: CGFloat = 1.0, color: UIColor = .darkGray) {
// figure out frame and resizing based on border type
var autoresizingMask: UIView.AutoresizingMask
var layerFrame: CGRect
switch borderType {
case .left:
layerFrame = CGRect(x: 0, y: 0, width: width, height: self.bounds.height)
autoresizingMask = [ .flexibleHeight, .flexibleRightMargin ]
case .right:
layerFrame = CGRect(x: self.bounds.width - width, y: 0, width: width, height: self.bounds.height)
autoresizingMask = [ .flexibleHeight, .flexibleLeftMargin ]
case .top:
layerFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: width)
autoresizingMask = [ .flexibleWidth, .flexibleBottomMargin ]
case .bottom:
layerFrame = CGRect(x: 0, y: self.bounds.height - width, width: self.bounds.width, height: width)
autoresizingMask = [ .flexibleWidth, .flexibleTopMargin ]
}
// look for the existing border in subviews
var newView: UIView?
for eachSubview in self.subviews {
if eachSubview.tag == borderType.rawValue {
newView = eachSubview
break
}
}
// set properties on existing view, or create a new one
if newView == nil {
newView = UIView(frame: layerFrame)
newView?.tag = borderType.rawValue
self.addSubview(newView!)
} else {
newView?.frame = layerFrame
}
newView?.backgroundColor = color
newView?.autoresizingMask = autoresizingMask
}

sizeToFit not working on a programmatic tableview cell

I have a basic custom cell with a name label on the left and a price label on the right both set inside another view to customise the spacing. I want the price to change width to whatever the price is and not have it set but when i use sizetofit on the price in the cells init or the cellForRow at function nothing happens. I have looked around but cant see how to get it to work. I cant get the text size when in the init of a cell but it doesnt seem right to be setting the label size within cellForRowAt.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SplitterCarouselItemTableViewCell = tableView.dequeueReusableCell(withIdentifier: "splitterCarouselItemTableViewCell") as! SplitterCarouselItemTableViewCell
var item = ((allBillSplitters[tableView.tag].items)?.allObjects as! [Item])[indexPath.row]
if allBillSplitters[tableView.tag].isMainBillSplitter {
getMainBillSplitterItems(splitter: allBillSplitters[tableView.tag])
item = mainBillSplitterItems[indexPath.row]
}
let count = item.billSplitters?.count
if count! > 1 {
cell.name!.text = "\(item.name!)\nsplit \(count!) ways"
cell.price!.text = "£\(Double(item.price)/Double(count!))"
} else {
cell.name!.text = item.name!
cell.price!.text = "£\(item.price)"
}
return cell
}
and heres my cell:
import UIKit
class SplitterCarouselItemTableViewCell: UITableViewCell {
var name: UILabel!
var price: UILabel!
var view: UIView!
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:)")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: "splitterCarouselItemTableViewCell")
self.setupViews()
}
func setupViews() {
let width = Int(UIScreen.main.bounds.width * 0.88)
let height = Int(self.bounds.height)
self.backgroundColor = .clear
self.frame = CGRect(x: 0, y: 0, width: width, height: 45)
view = UIView(frame: CGRect(x: 0, y: 2, width: width, height: height - 4 ))
view.backgroundColor = UIColor.white.withAlphaComponent(0.5)
price = UILabel(frame: CGRect(x: width - 80, y: 0, width: 75, height: height))
price.font = UIFont.systemFont(ofSize: 15.0)
price.backgroundColor = .yellow
price.textAlignment = .right
name = UILabel(frame: CGRect(x: 5, y: 0, width: Int(price.frame.width), height: height))
name.backgroundColor = .red
name.font = UIFont.systemFont(ofSize: 15.0)
name.numberOfLines = 0
view.addSubview(name)
view.addSubview(price)
contentView.addSubview(view)
}
}
Any help would be great, im sure im missing something basic.
Ive added the yellow and red backgrounds for visibility in the screen shot.
You need to calculate the width of the price label based on text. Below code will help you to find the width
extension String {
func widthWithConstrainedHeight(height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height )
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.width
}
}
Use above function to find string width and use that width to create frame of price label.
I'm unclear if you want the frame size to adjust or the font size. This answer assumes the latter, i.e. that you want a fixed frame width and a font that shrinks as needed to fit....
Try enabling adjustsFontSizeToFitWidth,
price = UILabel(frame: CGRect(x: width - 80, y: 0, width: 75, height: height))
price.font = UIFont.systemFont(ofSize: 15.0)
price.backgroundColor = .yellow
price.textAlignment = .right
price.adjustsFontSizeToFitWidth = true
this should shrink the font to fit the width of the label.

Set the position of Image View

I want to change the position of image view that is based on different devices.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier0, forIndexPath: indexPath) as! CelebrityHeaderCell
let CelebrityTopObj = self.mCelebrityTop[indexPath.item]
if CelebrityTopObj.video_pic != nil
{
SDWebImageManager.sharedManager().downloadImageWithURL(NSURL(string: CelebrityTopObj.video_pic), options: .LowPriority, progress: { (min:Int, max:Int) -> Void in
}) { (vimage:UIImage!, error:NSError!, cacheType:SDImageCacheType, finished:Bool, url:NSURL!) -> Void in
if vimage != nil && finished
{
if getScreenHeight()<=568 {
cell.VideoImg.image = imageResize(vimage, sizeChange: CGSize(width: self.screenWidth/2.5, height: self.screenWidth/3))
cell.PrevIcon.image = imageResize(UIImage(named: "icon_prev")!, sizeChange: CGSize(width: 25, height: 25))
cell.NextIcon.image = imageResize(UIImage(named: "icon_next")!, sizeChange: CGSize(width: 25, height: 25))
cell.TopName.font = cell.TopName.font.fontWithSize(15)
cell.PrevIcon.frame.origin.x = 10
cell.PrevIcon.frame.origin.y = 30
cell.PrevIcon.frame = CGRect(x: 10, y: 30, width: 25, height: 25)
} else {
cell.VideoImg.image = imageResize(vimage, sizeChange: CGSize(width: self.screenWidth/2, height: 100))
cell.PrevIcon.image = imageResize(UIImage(named: "icon_prev")!, sizeChange: CGSize(width: 55, height: 55))
cell.NextIcon.image = imageResize(UIImage(named: "icon_next")!, sizeChange: CGSize(width: 55, height: 55))
}
}
}
}
return cell
}
I want to set the position of PrevIcon by using frame.origin.x and frame.origin.y but it doesn't work. How do I do to set the position?
http://gfamily.cwgv.com.tw/public/images/iphone5.png
To set the new position try something like that:
cell.PrevIcon.frame = CGRect(x: 10, y: 50, width: cell.PrevIcon.frame.bounds.width, height: cell.PrevIcon.frame.bounds.width)
To change a parameter of the "frame" you have to set it with CGRect(). You cant edit just a single value.
Also have a look at size classes in storyboard. Size classes enables you to have different layout constraints for different device types and orientations. Here is a link to a quick tutorial.

Resources