Swift custom UICollectionViewCell subViews disappear when imageView image set at ViewController - ios

I faced weird issue while handling UICollectionView
I Created simple custom UICollectionViewCell, which has only one imageView and Label:
There's default placeholder image for Cell's imageView and updating imageView.image from collectionView(_:cellForItemAt:). But When image is set, all subview of cell disappears:
(Cells are not disappear at same time because downloading & setting image is async)
Note: Sample data I used is not wrong (Same data works for TableView in same app)
Why this happens and how can I fix it?
this is Sample data I used:
let movies = [
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955)
]
this is my part of ViewController:
lazy var collectionView = { () -> UICollectionView in
// FlowLayout
var flowLayout = UICollectionViewFlowLayout()
flowLayout.headerReferenceSize = CGSize(width: self.preferredContentSize.width, height: 180)
flowLayout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
flowLayout.minimumInteritemSpacing = 20
flowLayout.minimumLineSpacing = 20
// Collection View
var collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: flowLayout)
collectionView.register(DiscoverCollectionViewCell.self, forCellWithReuseIdentifier: identifiers.discover_collection_cell)
collectionView.register(DiscoverCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifiers.discover_collection_header)
collectionView.backgroundColor = UIColor(named: Colors.background)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Discover"
collectionView.dataSource = self
collectionView.delegate = self
self.view.backgroundColor = UIColor(named: Colors.background)
self.view.addSubview(collectionView)
collectionView.snp.makeConstraints { $0.edges.equalTo(self.view.safeAreaLayoutGuide) }
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Sample Cell
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifiers.discover_collection_cell, for: indexPath) as? DiscoverCollectionViewCell else { return DiscoverCollectionViewCell() }
let movie = movies[indexPath.row]
cell.movieTitle.text = movie.title
DispatchQueue.global().async {
guard let imageURL = URL(string: "https://image.tmdb.org/t/p/original/\(movie.posterPath)") else { return }
guard let imageData = try? Data(contentsOf: imageURL) else { return }
DispatchQueue.main.sync {
cell.posterImage.image = UIImage(data: imageData)
}
}
return cell
}
and this is my custom CollectionViewCell, I used Snapkit, Then library:
class DiscoverCollectionViewCell: UICollectionViewCell {
//MARK: Create properties
lazy var posterImage = UIImageView().then {
$0.image = UIImage(named: "img_placeholder")
$0.contentMode = .scaleAspectFit
}
lazy var movieTitle = UILabel().then {
$0.font = UIFont.systemFont(ofSize: 15)
$0.textColor = .white
$0.numberOfLines = 2
$0.minimumScaleFactor = 10
}
override init(frame: CGRect) {
super.init(frame: frame)
// add to view
self.addSubview(posterImage)
self.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
movieTitle.snp.makeConstraints { make in
make.top.equalTo(posterImage.snp.bottom).offset(5)
make.bottom.greaterThanOrEqualToSuperview()
make.leading.equalTo(posterImage.snp.leading)
make.trailing.equalTo(posterImage.snp.trailing)
}
self.backgroundColor = .blue
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Two issues with your cell's layout...
// add to view
self.addSubview(posterImage)
self.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
}
You should always add UI elements to the cell's .contentView, not to the cell itself.
You did not constrain the bottom of the image view.
// add to ContentView!
self.contentView.addSubview(posterImage)
self.contentView.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
make.top.left.right.bottom.equalToSuperview()
}
Edit
You were missing a couple things from your post (including how you're setting your cell / item size), so while the above changes do fix the image not showing at all, it's not quite what you're going for.
I'm assuming you're setting the flow layout .itemSize somewhere, so your original constraints - without adding .bottom. to the image view constraints - were close...
When you add an image to a UIImageView, the intrinsicContentSize becomes the size of the image. Your constraints are controlling the width, but...
This constraint on your label:
make.bottom.greaterThanOrEqualToSuperview()
means "put the Bottom of the label at the Bottom of its superview or farther down!"
When your image loads, it sets the image view Height to its own Height and pushes the label way down past the bottom of the cell.
That line needs to be:
make.bottom.equalToSuperview()
That will prevent the Bottom of the label from moving.
Next, you need to tell auto-layout "don't compress or stretch the label vertically":
// prevent label from stretching vertically
movieTitle.setContentHuggingPriority(.required, for: .vertical)
// prevent label from compressing vertically
movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
Without that, the label will be compressed down to Zero height.
I find it very helpful to add comments so I know what I'm expecting to happen:
override init(frame: CGRect) {
super.init(frame: frame)
// add to ContentView
self.contentView.addSubview(posterImage)
self.contentView.addSubview(movieTitle)
//MARK: Add Constraints
posterImage.snp.makeConstraints { make in
// constrain image view to
// Top / Left / Right of contentView
make.top.left.right.equalToSuperview()
}
// prevent label from stretching vertically
movieTitle.setContentHuggingPriority(.required, for: .vertical)
// prevent label from compressing vertically
movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
movieTitle.snp.makeConstraints { make in
// constrain Top of label to Bottom of image view
// because we've set Hugging and Compression Resistance on the label,
// this will "pull down" the bottom of the image view
make.top.equalTo(posterImage.snp.bottom).offset(5)
// constrain Bottom of label to Bottom of contentView
// must be EQUAL TO
//make.bottom.greaterThanOrEqualToSuperview()
make.bottom.equalToSuperview()
// Leading / Trailing equal to image view
make.leading.equalTo(posterImage.snp.leading)
make.trailing.equalTo(posterImage.snp.trailing)
}
self.backgroundColor = .blue
}
Now we get this result:
and after the images download:
One final thing - although you may have already done something to address this...
As you see in those screenshots, setting .numberOfLines = 2 on a label does not force a 2-line height... it only limits it to 2 lines. If a Movie Title is short, the label height will be shorter as seen in the 2nd cell.
One way to fix that would be to constrain the label height to something like 2.5 lines by adding this to your init:
if let font = movieTitle.font {
movieTitle.snp.makeConstraints { make in
make.height.equalTo(font.lineHeight * 2.5)
}
}
That will give this output:

Although I am not sure, Because collection view cells are being reused the init of cells only gets called at the first time, not the time when image data is getting loaded from the server.
Try moving your layout-related code(specifically adding subviews and constraining them) in a different method of the cell and call it every time image gets loaded.

Related

Snapkit LayoutConstraints Unable to simultaneously satisfy constraints

I have simple UITableView and custom UITablViewCell in my project, which created programmatically and use SnapKit for auto layout its.
When I run app, everything work fine and get some LayoutConstraints error in debug console.
UITableViewCell
import UIKit
import SnapKit
class TestTableViewCell: UITableViewCell {
...
private lazy var containerView: UIView = {
let view = UIView()
return view
}()
private lazy var centerStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [titleLabel, disclosureImageView])
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 5.0
return stackView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
return label
}()
private lazy var disclosureImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "disclosure")
return imageView
}()
..
private func setupUI() {
selectionStyle = .none
backgroundColor = .background
contentView.addSubview(containerView)
containerView.backgroundColor = .white
containerView.layer.cornerRadius = 12
containerView.clipsToBounds = true
containerView.addSubview(centerStackView)
setNeedsUpdateConstraints()
}
// MARK: - Update Constraints
override func updateConstraints() {
containerView.snp.updateConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(100)
}
centerStackView.snp.updateConstraints { make in
make.center.equalToSuperview()
}
super.updateConstraints()
}
}
How I can fix this constraint error ?
There are actually two conflicts for each table view cell. You can copy-paste the sets of constraints are in conflict into https://www.wtfautolayout.com and see exactly why they are conflicting. As a result, we get this and this.
We can see that the first conflict is caused by your container view having a height constraint, but the table view itself also adds a height constraint to the cell to enforce its cells' sizes,
"<NSLayoutConstraint:0x7b1400031920 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7b4c00003640.height == 116.5 (active)>"
For some reason, the constant here is 116.5, not 116 as you would expect from the 100 points of height of the container view + the 16 points of inset.
You can either change it to
make.height.greaterThanOrEqualTo(100)
Or remove the height constraint completely:
// make.height.equalTo(100)
and use the delegate method heightForRowAt to control the cell height instead:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
116
}
As for the second conflict, it is caused by a constraint constraining the stack view's width to 0, being in conflict with the fact that there must be some spacing between the image and label.
"<NSLayoutConstraint:0x7b1400031b50 'UIView-Encapsulated-Layout-Width' UIStackView:0x7b4c000039c0.width == 0 (active)>"
If you move the following code from updateConstraints into setupUI, the conflict is resolved:
containerView.snp.updateConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.height.greaterThanOrEqualTo(100)
}
centerStackView.snp.updateConstraints { make in
make.center.equalToSuperview()
}

UITableViewCell with a StackView inside with dynamically added WKWebViews and UIImageViews

As the title says, I'm trying to display the following layout:
As you see, the dynamic stack view is a container where content is added dynamically. This content is variable and is decided on run time. Basically, it can be webviews (with variable content inside), ImageViews (with variable height), and videos (this view would have a fixed view).
I configured the CellView with automatic row height, and provided an estimated row height, both in code and in Xcode. Then on the tableView_cellForRow at the method of the ViewController, the cell is dequeued and the cell is rendered with content.
During this setup process, the different labels and views are filled with content, and the dynamic container too. The webviews are added to the stackview with the following code:
var webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.isScrollEnabled = false
webView.navigationDelegate = myNavigationDelegate
webView = addContentToWebView(content, webView)
container.addArrangedSubview(webView)
I'm testing this with only a webview inside the stackview and having already problems with the height of the row.
The webview is rendered correctly inside the stackview, but not completely (the webview was bigger as the estimated rowheight). I used the navigation delegate to calculate the height of the added webview and resize the StackContainer accordingly, with the following code:
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let h = height as! CGFloat
print("Height 3 is \(h)")
self.dynamicContainerHeightContraint.constant = h
})
}
})
And indeed, the stackcontainer is resized and expanded to match the height of the webview that is inside.
But the row remains with the same estimated height, and if the webview is very big in height, then all the other views disappear (they are pushed outside the bounds of the row.
Is there a way to tell the row to autoresize and adapt to its contents? Or maybe I'm using the false approach?
I suppose the problem is that the height of the views added to the stackview is not known in advance, but I was expecting a way to tell the row to recalculate its height after adding all the needed stuff inside...
Thank you in advance.
Table views do not automatically redraw their cells when a cell's content changes.
Since you are changing the constant of your cell's dynamicContainerHeightContraint after the cell has been rendered (your web view's page load is asynchronous), the table does not auto-update -- as you've seen.
To fix this, you can add a "callback" closure to your cell, which will let the cell tell the controller to recalculate the layout.
Here is a simple example to demonstrate.
The cell has a single label... it has a "label height constraint" var that initially sets the height of the label to 30.
For the 3rd row, we'll set a 3-second timer to simulate the delayed page load in your web view. After 3 seconds, the cell's code will change the height constant to 80.
Here's how it looks to start:
Without the callback closure, here's how it looks after 3 seconds:
With the callback closure, here's how it looks after 3 seconds:
And here's the sample code.
DelayedCell UITableViewCell class
class DelayedCell: UITableViewCell {
let myLabel = UILabel()
var heightConstraint: NSLayoutConstraint!
// closure to tell the controller our content changed height
var callback: (() -> ())?
var timer: Timer?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.clipsToBounds = true
myLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(myLabel)
let g = contentView.layoutMarginsGuide
// we'll change this dynamically
heightConstraint = myLabel.heightAnchor.constraint(equalToConstant: 30.0)
// use bottom anchor with Prioirty: 999 to avoid auto-layout complaints
let bc = myLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
bc.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain label to all 4 sides
myLabel.topAnchor.constraint(equalTo: g.topAnchor),
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// activate bottom and height constraints
bc,
heightConstraint,
])
}
func fillData(_ str: String, testTimer: Bool) -> Void {
myLabel.text = str
// so we can see the label frame
// green if we're testing the timer in this cell
// otherwise yellow
myLabel.backgroundColor = testTimer ? .green : .yellow
if testTimer {
// trigger a timer in 3 seconds to change the height of the label
// simulating the delayed load of the web view
timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.heightChanged), userInfo: nil, repeats: false)
}
}
#objc func heightChanged() -> Void {
// change the height constraint
heightConstraint.constant = 80
myLabel.text = "Height changed to 80"
// run this example first with the next line commented
// then run it again but un-comment the next line
// tell the controller we need to update
//callback?()
}
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
timer?.invalidate()
}
}
}
DelayTestTableViewController UITableViewController class
class DelayTestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DelayedCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DelayedCell
// we'll test the delayed content height change for row 2
let bTest = indexPath.row == 2
cell.fillData("Row \(indexPath.row)", testTimer: bTest)
// set the callback closure
cell.callback = { [weak tableView] in
guard let tv = tableView else { return }
// this will tell the tableView to recalculate row heights
// without reloading the cells
tv.performBatchUpdates(nil, completion: nil)
}
return cell
}
}
In your code, you would make the closure callback after this line:
self.dynamicContainerHeightContraint.constant = h

Swift - StackView and Adding Constraints With Programmatically Added Views To Stack - Adjust Cell Hight After Load?

I'm trying to make a custom tableView cell that has a stackView from an array images.
I used interface builder and added a horizontal stackView with:
Center X
Center Y
Top +12
Bottom +12
I'm using a loop to add Arranged Subview and it works fine. But the images are super small, even though they shouldn't be due to their CGRect. But they are getting adjusted by the stackView.
The idea was to keep the stack centered, while also using the top/bottom to adjust the hight of the cell.
I've reasoned that the issue I'm facing with the images being small is because the stackView is initialized empty. So the stackViews hight is very tiny. And because imageViews are being added inside of it, they are then being resized to fit.
How can you get a cell/stackView to adjust their hight after being displayed?
Note: I've also been having issues with trying to add a top & bottom constraint to the imageView programmatically.
for pictogram in pictograms {
let imageView = UIImageView()
let size = self.bounds.width / CGFloat(pictograms.count + 1)
print("size: ", size)
imageView.frame = CGRect(x: 0, y: 0, width: size, height: size)
imageView.contentMode = .scaleAspectFit
imageView.image = pictogram.image
pictogramStack.addArrangedSubview(imageView)
}
You are using a UIStackView in the wrong way. Stack views will arrange the subviews for you - no need to be calculating widths and setting frames.
Layout your cell prototype like this:
Note that the Bottom constraint has Priority: 999. Auto-layout needs to make multiple "passes" to lay out stack views (particularly in table view cells). Using a priority of 999 for the bottom constraint will avoid Constraint Conflict error / warning messages.
Set the stack view properties like this:
With Distribution: Fill Equally we don't have to do any let size = self.bounds.width / CGFloat(pictograms.count + 1) kind of calculations.
Also, to make design-time a little easier, give the stack view a Placeholder intrinsic height:
That will have no effect at run-time, but allows you to clearly see your cell elements at design time.
Now, when you "fill" the stack view with image views, no .frame = setting, and the only constraint you need to add is Height == Width:
NSLayoutConstraint.activate([
// image view should have 1:1 ratio
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
])
Here is a complete example:
class HorizontalImagesStackCell: UITableViewCell {
#IBOutlet var theStack: UIStackView!
func addImages(_ pictograms: [String]) -> Void {
// cells are reused, so remove any previously added image views
theStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
pictograms.forEach { s in
// make sure we can load the image
if let img = UIImage(named: s) {
// instantiate an image view
let imageView = UIImageView()
// give it a background color so we can see its frame
imageView.backgroundColor = .green
// scale aspect fit
imageView.contentMode = .scaleAspectFit
// set the image
imageView.image = img
NSLayoutConstraint.activate([
// image view should have 1:1 ratio
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
])
// add it to the stack
theStack.addArrangedSubview(imageView)
}
}
}
}
class ImagesInStackTableViewController: UITableViewController {
// we'll display 5 rows of images
// going from 5 images to 4 images ... to 1 image
let myData: [Int] = [5, 4, 3, 2, 1]
let myImages: [String] = [
"img1", "img2", "img3", "img4", "img5"
]
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HCell", for: indexPath) as! HorizontalImagesStackCell
// get the first n number of images
let a: [String] = Array(myImages.prefix(myData[indexPath.row]))
cell.addImages(a)
return cell
}
}
Using these 5 images:
We get this result (image views are set to .scaleAspectFit and have green backgrounds so we can see the frames):

UIStackView dynamcially add 2 labels next to each other with no extra spacing

Im new to iOS development and Im a bit confused as to how to achieve this.
I have 2 UILabels added to a UIStackView like so:
let horizontalStackView1 = UIStackView(arrangedSubviews: [self.label1, self.label2])
and when I run the app it looks like this:
However, Id like the labels to be next to each other with no spacing in between something like this:
Ive tried setting horizontalStackView1.distribution, horizontalStackView1.alignment etc with no luck.
How do I achieve this?
Thanks in advance!
UPDATE:
The code looks like this (its a cell of a table by the way):
class ItemTableViewCell: UITableViewCell
{
...
let stateLabel = UILabel()
let effectiveDateLabel = UILabel()
...
var typeImage: UIImage?
{
...
}
var summary: String?
{
...
}
var effectiveDate: Date?
{
...
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.accessoryType = .disclosureIndicator
...
let horizontalStackView1 = UIStackView(arrangedSubviews: [self.stateLabel, self.effectiveDateLabel])
let horizontalStackView2 = UIStackView(arrangedSubviews: [typeImageViewWrapper, self.titleLabel])
horizontalStackView2.spacing = 4
let verticalStackView = UIStackView(arrangedSubviews: [horizontalStackView1, horizontalStackView2, self.summaryLabel])
verticalStackView.axis = .vertical
verticalStackView.spacing = 4
self.contentView.addSubview(verticalStackView)
...
}
required init?(coder aDecoder: NSCoder)
{
fatalError()
}
}
That's because the UIStackView picks the first arrangedSubview with lowest content hugging priority and resizes it so the stackview's content takes up full width.
If you want to use UIStackView for this case, you can should change the content hugging priorities, eg.
label2.setContentHuggingPriority(.defaultLow, for: .horizontal)
label1.setContentHuggingPriority(.defaultHigh, for: .horizontal)
The stackviews distribution should be set to fillProportionally so every arranged subview keeps its proportions.
However, the remaining space is filled by the stackview automatically. To suppress this, you need to add an empty view at the end. This empty view needs a low content hugging priority so it can grow to fill up the space where the other views remain by their proportions.
Furthermore, the empty view needs an intrinsicContentSize for the stackview to compute the dimensions:
class FillView: UIView {
override var intrinsicContentSize: CGSize {
get { return CGSize(width: 100, height: 100) }
}
}
Now set your arranged subviews and put the fillView at the end
let fillView: UIFillView()
fillView.setContentHuggingPriority(priority: .fittingSizeLevel, for: .horizontal)
myStackView.addArrangedSubview(fillView)
Set the stackviews spacing to your needs to maintain the distance between the subviews.

Dynamically adjust height of UITableViewCell fail in Swift 3

I have a customized UITableViewCell,and I want it to update height accroding my label text content.
But I try to use this code in my ViewController ViewDidLoad:
tableView.register(ChatRightTextTableViewCell.self, forCellReuseIdentifier: ChatRightTextTableViewCell.identifier)
tableView.estimatedRowHeight = 100.0
tableView.rowHeight = UITableViewAutomaticDimension
it doesn't seem to update the height.
What's wrong with me about my cell constraint?
Thanks.
import snapKit
import UIKit
class ChatRightTextTableViewCell: UITableViewCell {
static let identifier = "ChatRightTextTableViewCell"
var cellHeight: CGFloat = 0
var labelContent:UILabel = { ()->UILabel in
let ui:UILabel = GeneratorLabel()
ui.textColor = UIColor(red:0.20, green:0.20, blue:0.20, alpha:1.00)
ui.backgroundColor = UIColor.white
ui.font = defaultTextFont
ui.numberOfLines = 0
ui.lineBreakMode = NSLineBreakMode.byCharWrapping
ui.layer.cornerRadius = defaultButtonRadius
ui.layer.masksToBounds = true
return ui
}()
var labelDatetime:UILabel = { ()->UILabel in
let ui = GeneratorLabel()
ui.font = defaultMessageTimeFont
ui.textColor = defaultChatTimeColor
ui.numberOfLines = 0
return ui
}()
var icon:UIImageView = { ()->UIImageView in
let ui = GeneratorAvatarImageView()
ui.isUserInteractionEnabled = true
return ui
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
loadContent()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
loadVFL()
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func loadContent() {
backgroundColor = defaultBackgroundColor
selectionStyle = .none
contentView.addSubview(labelContent)
contentView.addSubview(labelDatetime)
contentView.addSubview(icon)
}
func loadVFL() {
let datetimeHeight:CGFloat = UIScreen.main.bounds.height*0.0195 //13
let contentWidth:CGFloat = contentView.bounds.width*0.62 //254.5
let iconLeftPadding:CGFloat = contentView.bounds.width*0.0266 //15
let contentAndDatePadding:CGFloat = UIScreen.main.bounds.height*0.0089 //6
let dateAndBottomPaddding:CGFloat = UIScreen.main.bounds.height*0.0090 //6
let topPadding:CGFloat = UIScreen.main.bounds.height*0.0149 //10
let nameLabelBottomPadding:CGFloat = UIScreen.main.bounds.height*0.0075 //5
let views = DictionaryOfInstanceVariables(self, objects: "labelContent","labelDatetime","icon")
let metrics = ["padding":iconLeftPadding,"contentAndDatePadding":contentAndDatePadding,"dateAndBottomPaddding":dateAndBottomPaddding,"nameLabelBottomPadding":nameLabelBottomPadding,"topPadding":topPadding]
labelContent.frame = CGRect(x: 0, y: 0, width: contentWidth, height: CGFloat.greatestFiniteMagnitude)
labelContent.sizeToFit()
icon.snp.makeConstraints { (make) -> Void in
make.width.equalTo(defaultChatroomIconWidth)
make.height.equalTo(defaultChatroomIconWidth)
make.centerY.equalToSuperview()
}
labelDatetime.snp.makeConstraints { (make) in
make.height.equalTo(datetimeHeight)
}
labelContent.backgroundColor = UIColor.red
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[labelContent]-padding-[icon]-padding-|", options: [], metrics: metrics, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[labelDatetime]-padding-[icon]-padding-|", options: [], metrics: metrics, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-topPadding-[labelContent]-contentAndDatePadding-[labelDatetime]-dateAndBottomPaddding-|", options: [], metrics: metrics, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-topPadding-[icon]", options: [], metrics: metrics, views: views))
}
}
Gotcha,
If u had used a xib, you would have noticed the warning, because u have coded it and placed labels its difficult to guess the issue :)
You are using two labels which basically decides the height of the cell based on the content they show. But because both have Same Content Hugging priority on vertical axis, providing only top and bottom constraint to these labels will not suffice.
You have to set the content hugging priority over vertical axis to one of the label (which ever u think will have smaller content obviously) to higher value to provide enough data to calculate the dynamic height of the cell.
Summary :
Simply set
labelDatetime.setContentHuggingPriority(252, for: .vertical)
or
var labelDatetime:UILabel = { ()->UILabel in
let ui = GeneratorLabel()
ui.font = defaultMessageTimeFont
ui.textColor = defaultChatTimeColor
ui.numberOfLines = 0
ui..setContentHuggingPriority(252, for: .vertical)
return ui
}()
Autolayout would usually provide a warning in the case that you were using a xib. Here's your issue: Content Hugging Priority & Content Compression Resistance.
Content Hugging Priority is a value that indicates wether the content of a view should have priority to stretch it's superview over the superview's own constraints. In this case, your label doesn't have priority over the cell content view to stretch it, so it's getting squished.
Content Compression Resistance is, similarly, a value that indicates wether the content of a view has priority to limit it's superview's frame based on it's own. For example, if your cell wanted to go to height 1, but your label had a higher content compression resistance and a height of, let's say 2, the cell would be forced to limit itself to two.
A good explanation of this is provided here.
Let's solve your problem then.
Either assign a bigger content hugging priority, or content compression resistance. In your case it seems like the label doesn't have priority to stretch the cell, so set the hugging priority:
labelDatetime.setContentHuggingPriority(500, for: .vertical)
Alternatively, experiment with content compression to give the cell priority over controlling it's own height, while respecting the label's boundaries.
Follows the below steps make your UITableCell height as dynamic according to the contents. That is very easy way to achieve all the steps.
Remove all the constraint from your UITableViewCell.
Remove all the programing constraint.
Now Set All the constraint from the UITableViewCell as given in diagram.
Means give all constraint left, right, top and bottom.
If you want background of label then use your inspector of attribute from right side of your xCode.
if you want to use radios then use identity inspector tab and add USER DEFINED RUNTIME ATTRIBUTES key path -> layer.cornerRadius, type -> Number and value -> 5 / or what you want to give.
if you want to give number of line to label then give it from the Attribute Inspector as choose as multipleLine alignment.
If you don't want then give number of line 0 and choose as multipleLine alignment and check Automatically Adjust Font.
Now Lets constraint on programming
//Set Both line in view did upload function with same format
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 100.0
Remove Function if you have use
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
}

Resources