UIStackView, Autolayout, Table Cells - swift

I'm creating a UIStackView with an image and a label in a custom UIControl that will be in a custom UITableViewCell, except that my UIStackView has a height & width of 0 and XCode is complaining about breaking constraints. Only if I explicitly set a height and width does it show properly, which I don't want because the label text varies from cell to cell. (This is all happening programmatically.)
The Setup
In my UITableViewCell, I've got the following:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(control)
NSLayoutConstraint.activate([
control.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
control.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
control.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
control.topAnchor.constraint(equalTo: contentView.topAnchor),
])
}
// empty coder init as well
private let control: MyControl = {
let control: MyControl = MyControl()
control.translatesAutoresizingMaskIntoConstraints = false
return control
}()
In MyControl, I just have the UIStackView, a UIImageView, and a UILabel. To not bore you with code...only the UIStackView (horizontal axis) is using constraints, pinning it to the four sides. The UIImageView (initiated with an image) is one arranged subview, the UILabel is the other (initiated with default text).
If you want to see the code:
class MyControl: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(stackView)
stackView.addArrangedSubview(icon)
stackView.addArrangedSubview(contentLabel)
NSLayoutConstraint.activate([
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
])
}
// empty coder init as well
private let contentLabel: UILabel = {
let label: UILabel = UILabel()
label.text = "Initial text"
return label
}()
private let icon: UIImageView = {
let iv: UIImageView = UIImageView(image: UIImage(named: "placeholder_image")!)
iv.contentMode = .scaleAspectFit
return iv
}()
private let stackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
return control
}()
}
What I'm Expecting
I'm expecting the UIControl to be the height of the image (because it's taller than the label text), and the width of the image + the label text. (And then display full table cell width because of that constraint pinning). And because these are set on initialization of these components, I'd expect them to have an intrinsic height and width to pass to the UIStackView.
What I'm Getting
A height and width of zero, and XCode complaints of broken constraints. If I remove all constraints, I get no complaints but nothing appears (as if the height & width are zero but XCode doesn't care because I didn't set any constraints).
What I've Tried
Literally every combination of layout constraints, including none on everything and as many as I can on everything. What I'd like is for the image + label text to set the height and width of the UIStackView which would then set the height and width of the UIControl, which would then set the height in the UITableViewCell (I know I have width 100% - that will change later).
Other Considerations
There's nothing else special about my UITableViewCell that would cause any issue here except that in my actual code, I have a multi-line label above MyControl which should (and does) cause my UITableViewCell to expand in height.

The problem with your question is that so much of the code you've shown is bogus that it's hard to guess what you might actually be doing. You claim that in your table view cell (I presume it's a table view cell subclass) you are saying
override init(frame: CGRect) {
super.init(frame: frame)
But that would never compile, as init(frame:) is not the designated initializer for a UITableViewCell. You have code like this:
private let contentLabel: UILabel = {
let label: UILabel = UILabel()
label.text = "Initial text"
return label
}
But that would never compile, as a function is not a label.
If we make allowances for all of that and fix your code, it's difficult to see what you would be doing wrong. I corrected your code so that it would compile, and I got this in my table view:
That might not be exactly what you were after, but the image view is certainly sizing the cells to its own height.
What you are doing is not at all how one makes and configures a table view cell (you should be doing the work in cellForRowAt, not hard coding the cell's image view and label contents in the cell's initializer), but given what you've shown, the image view does size the stack view which does size the cell (contrary to my own initial expectations).
Another issue is that you can't put an image view and a label into an autolayout situation without resolving the ambiguity as to which should predominate. I added this line to do that:
iv.setContentHuggingPriority(.defaultLow+1, for: .horizontal)

Related

AutoLayout doesn't work for custom UIView [duplicate]

Im working on a chat app, so the height of the rows varies. I am using separate cells for plain txt msg and msg with image (and maybe text). I allow the user to select an image from the phone. I display that image in a separate VC where he can enter text if he chooses and send it. I have a model for the msg which means I do conversion between base64 and image formats. I have tried to simplify to the max my cell class to understand the following problem: the image inside the image view appears zoomed beyond what the normal phone zoom would allow; and the height of the cell is immense. On the cell class that I need to use I have more items and constraints but the basic logic of interest here is below:
fileprivate func configureMsgsTable() {
tableView.contentInsetAdjustmentBehavior = .automatic
tableView.backgroundColor = .clear
tableView.keyboardDismissMode = .interactive
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
tableView.sectionFooterHeight = 0.0
}
these are the functions I use for encoding/decoding:
fileprivate func convertImageToBase64String (img: UIImage) -> String {
let imageData:NSData = img.jpegData(compressionQuality: 0.50)! as NSData
let imgString = imageData.base64EncodedString(options: Data.Base64EncodingOptions.lineLength64Characters)
return imgString
}
fileprivate func convertBase64StringToImage (imageBase64String:String) -> UIImage {
let imageData = Data.init(base64Encoded: imageBase64String, options: Data.Base64DecodingOptions.ignoreUnknownCharacters)
let image = UIImage(data: imageData!)
return image!
}
and this is the cell class.
class MsgWithImg: UITableViewCell {
//MARK: - Observer.
internal var valuesToDisplay: NewProjectGrpMsgModel! {
didSet {
imgView.image = convertBase64StringToImage(imageBase64String: valuesToDisplay.msgAttachment)
}
}
//MARK: - Properties.
fileprivate let imgView: UIImageView = {
let imgView = UIImageView(frame: .zero)
imgView.clipsToBounds = true
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.contentMode = .scaleAspectFill
imgView.layer.cornerRadius = 0
return imgView
}()
//MARK: - Init.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
clipsToBounds = true
selectionStyle = .none
setupViews()
}
required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
fileprivate func setupViews() {
contentView.addSubview(imgView)
let imgViewConstraints = [
imgView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 3),
imgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -3),
imgView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -45)
]
NSLayoutConstraint.activate(imgViewConstraints)
}
}
I spent some time thinking that this is an auto layout problem; or the fact that the table row height is automatic. that's why I built this test cell class with only the image view. but I think this problem is of a different nature. I did read quite a few answers to what I could find relevant on this website but I cannot determine what the problem is and the console does not print out anything.
The problem is that if you don't give a UIImageView both a width and a height, its intrinsicContentSize becomes the size of the image assigned to it.
With your code as-is, you've given the image view a width by constraining its Leading and Trailing anchors, but you haven't given it a height -- either by itself or by the cell's height (since you want auto-sizing cells).
So, if we use these four images:
The resulting table view looks like this:
And here's what's happening on an iPhone 13 (note: all sizes are rounded)...
For the 100x200 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (200-pts), setting the image view frame 200-pts tall
image view is set to .scaleAspectFill, so the scaled size is 345 x 690
For the 100x300 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (300-pts), setting the image view frame 300-pts tall
image view is set to .scaleAspectFill, so the scaled size is 345 x 1035
For the 600x200 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (200-pts), setting the image view frame 200-pts tall
image view is set to .scaleAspectFill, so the scaled size is 600 x 200
For the 800x600 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (600-pts), setting the image view frame 600-pts tall
image view is set to .scaleAspectFill, so the scaled size is 800 x 600
It may be clearer if we set the image view to .scaleAspectFit (with a red background so we can see the frame):
As a general rule, it is common to give the image view a fixed size (or proportional size), and use .scaleAspectFit to show the complete images. Or, also common, to use a pre-processor to generate "thumbnail" sized images for the table view cells.

Swift 5 table view cell with UIImage appears very tall and image extremely zoomed

Im working on a chat app, so the height of the rows varies. I am using separate cells for plain txt msg and msg with image (and maybe text). I allow the user to select an image from the phone. I display that image in a separate VC where he can enter text if he chooses and send it. I have a model for the msg which means I do conversion between base64 and image formats. I have tried to simplify to the max my cell class to understand the following problem: the image inside the image view appears zoomed beyond what the normal phone zoom would allow; and the height of the cell is immense. On the cell class that I need to use I have more items and constraints but the basic logic of interest here is below:
fileprivate func configureMsgsTable() {
tableView.contentInsetAdjustmentBehavior = .automatic
tableView.backgroundColor = .clear
tableView.keyboardDismissMode = .interactive
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
tableView.sectionFooterHeight = 0.0
}
these are the functions I use for encoding/decoding:
fileprivate func convertImageToBase64String (img: UIImage) -> String {
let imageData:NSData = img.jpegData(compressionQuality: 0.50)! as NSData
let imgString = imageData.base64EncodedString(options: Data.Base64EncodingOptions.lineLength64Characters)
return imgString
}
fileprivate func convertBase64StringToImage (imageBase64String:String) -> UIImage {
let imageData = Data.init(base64Encoded: imageBase64String, options: Data.Base64DecodingOptions.ignoreUnknownCharacters)
let image = UIImage(data: imageData!)
return image!
}
and this is the cell class.
class MsgWithImg: UITableViewCell {
//MARK: - Observer.
internal var valuesToDisplay: NewProjectGrpMsgModel! {
didSet {
imgView.image = convertBase64StringToImage(imageBase64String: valuesToDisplay.msgAttachment)
}
}
//MARK: - Properties.
fileprivate let imgView: UIImageView = {
let imgView = UIImageView(frame: .zero)
imgView.clipsToBounds = true
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.contentMode = .scaleAspectFill
imgView.layer.cornerRadius = 0
return imgView
}()
//MARK: - Init.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
clipsToBounds = true
selectionStyle = .none
setupViews()
}
required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
fileprivate func setupViews() {
contentView.addSubview(imgView)
let imgViewConstraints = [
imgView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 3),
imgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -3),
imgView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imgView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -45)
]
NSLayoutConstraint.activate(imgViewConstraints)
}
}
I spent some time thinking that this is an auto layout problem; or the fact that the table row height is automatic. that's why I built this test cell class with only the image view. but I think this problem is of a different nature. I did read quite a few answers to what I could find relevant on this website but I cannot determine what the problem is and the console does not print out anything.
The problem is that if you don't give a UIImageView both a width and a height, its intrinsicContentSize becomes the size of the image assigned to it.
With your code as-is, you've given the image view a width by constraining its Leading and Trailing anchors, but you haven't given it a height -- either by itself or by the cell's height (since you want auto-sizing cells).
So, if we use these four images:
The resulting table view looks like this:
And here's what's happening on an iPhone 13 (note: all sizes are rounded)...
For the 100x200 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (200-pts), setting the image view frame 200-pts tall
image view is set to .scaleAspectFill, so the scaled size is 345 x 690
For the 100x300 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (300-pts), setting the image view frame 300-pts tall
image view is set to .scaleAspectFill, so the scaled size is 345 x 1035
For the 600x200 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (200-pts), setting the image view frame 200-pts tall
image view is set to .scaleAspectFill, so the scaled size is 600 x 200
For the 800x600 image:
your Leading/Trailing constraints make the image view frame 345-pts wide
no height set, so auto-layout uses the image size (600-pts), setting the image view frame 600-pts tall
image view is set to .scaleAspectFill, so the scaled size is 800 x 600
It may be clearer if we set the image view to .scaleAspectFit (with a red background so we can see the frame):
As a general rule, it is common to give the image view a fixed size (or proportional size), and use .scaleAspectFit to show the complete images. Or, also common, to use a pre-processor to generate "thumbnail" sized images for the table view cells.

UIStackView and a placeholder view in another UIStackView problem

There is a problem if you have a UIStackView(testStack) and a placeholder UIView(testView) inside another UIStackView(mainStack). It is meant that if there is no content in the testStack it will collapse, and the testView will take all the space. There is even a content hugging priority set to maximum for the testStack so it should collapse its height to 0 when there are no subviews. But it does not. How to make it collapse when there is no content?
PS If there are items in the testStack, everything works as expected: testView takes all available space, testStack takes only the space to fit its subviews.
class AView: UIView {
lazy var mainStack: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.backgroundColor = .gray
stack.addArrangedSubview(self.testStack)
stack.addArrangedSubview(self.testView)
return stack
}()
let testStack: UIStackView = {
let stack = UIStackView()
stack.backgroundColor = .blue
stack.setContentHuggingPriority(.init(1000), for: .vertical)
return stack
}()
let testView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
init() {
super.init(frame: .zero)
backgroundColor = .yellow
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.topAnchor.constraint(equalTo: topAnchor).isActive = true
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When auto-layout arranges subviews in a UIStackView, it looks at:
the stack view's .distribution property
the subviews' height constraints (if given)
the subviews' Intrinsic Content Size
Since you have not specified a .distribution property, mainStack is using the default of .fill.
A UIStackView has NO Intrinsic Content Size, so auto-layout says "testStack has a height of Zero"
A UIView has NO Intrinsic Content Size, so auto-layout says "testView has a height of Zero"
Since the distribution is fill, auto-layout effectively says: "the heights of the arranged subviews are ambiguous, so let's give the last subview a height of Zero, and fill mainStack with the first subview.
Setting .setContentHuggingPriority will have no effect, because there is no intrinsic height to "hug."
If you set mainStack's .distribution = .fillEqually, you will get (as expected) testStack filling the top half, and testView filling the bottom half.
If you set mainStack's .distribution = .fillProportionally, you will get the same result... testStack filling the top half, and testView filling the bottom half, because .fillProportionally uses the arranged subviews' Intrinsic Content Sizes... in this case, they are both Zero, so "proportional" will be equal.
If you set mainStack's .distribution = .equalSpacing or .distribution = .equalCentering, you won't see either testStack or testView ... auto-layout will give each of them a height of Zero, and fill the rest of mainStack with (empty) "spacing."
If your goal is to have testStack "disappear" if it is empty, you can either:
set it hidden, or
subclass it and give it an intrinsic height

Obtaining UILabel width when not occupying full screen width?

I would like to obtain the width of a UILabel added as a subView inside a custom TableView Cell. The TableView Class I am using is listed below:
import UIKit
class customTableViewCell: UITableViewCell {
let customLabel: UILabel = {
let label = UILabel()
label.translateAutoConstraints = false
label.textAlignment = .left
return label
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(customLabel)
customLabel.topAnchor.constraint(equal: contentView.topAnchor).isActive = true
customLabel.leftAnchor.constraint(equal: contentView.leftAnchor, constant: 10).isActive = true
customLabel.rightAnchor.constraint(equal: contentView.rightAnchor, constant: (contentView.frame.size.width/2)-10-customLabel.frame.size.width).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}
However, from the above code when I assigned the rightAnchor constraint for the customLabel UILabel, Xcode did not return the correct width of the UILabel I was looking for.
I understand that I only specified the top and left constraints for the UILabel. I also know that UILabel by default has intrinsic layout on, that it can decide on the required height based on the content of the UILabel. However, I am wondering if I did not set the numberOfLines for the customUILabel defined in the above code as 0 (i.e., I only want my text inside the UILabel to occupy one line only). Can I obtain the width of the customUILabel before the text got truncated.
Let me explain further, if my customLabel has a lot of text, it will occupy the full width of the screen then gets truncated. However, if it does not contain a lot of text, then it’s width will be less than the width of the screen. And this is exactly what I am interested in obtaining, the width of the UILabel used to display the small text inside it?
Regards,
Shadi.
You need
self.contentView.rightAnchor.constraintGreaterThanOrEqualToAnchor(customLabel.rightAnchor, constant: 8.0).active = true
Then print the width inside
override func layoutSubviews() {
super.layoutSubviews()
print(customLabel.frame.width)
}
Don't use frames in constraints calculations
(contentView.frame.size.width/2)-10-customLabel.frame.size.width
Inside cell init they not yet calculated

ContentView for tableViewCell not auto-resizing correctly?

I have added 2 labels to my cell and setup these constraints with snapkit, issue is I cant get the cell to expand correctly, it stays at its default height:
titleLabel.snp.makeConstraints { (make) -> Void in
make.top.equalTo(contentView.snp.top)
make.bottom.equalTo(descriptionLabel.snp.top)
make.left.equalTo(contentView.snp.left)
make.right.equalTo(contentView.snp.right)
}
descriptionLabel.snp.makeConstraints { (make) -> Void in
make.top.equalTo(titleLabel.snp.bottom)
make.bottom.equalTo(contentView.snp.bottom)
make.left.equalTo(contentView.snp.left)
make.right.equalTo(contentView.snp.right)
}
I mapped the four edges as you can see, however I know height isnt implied by these, how can I apply a height when the content is by nature, dynamic, and could be various heights...
setup for the labels looks like this:
lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.textColor = .green
titleLabel.textAlignment = .center
contentView.addSubview(titleLabel)
return titleLabel
}()
lazy var descriptionLabel: UILabel = {
let descriptionLabel = UILabel()
descriptionLabel.textColor = .dark
descriptionLabel.textAlignment = .center
descriptionLabel.numberOfLines = 0
contentView.addSubview(descriptionLabel)
return descriptionLabel
}()
Give the table view an estimatedRowHeight, and set its rowHeight to UITableViewAutomaticDimension. Now the cells will be self-sizing. Well, if a label is pinned by all four sides to the content view, and if the cell is self-sizing, then that's all you have to do: the label will automatically change its height to accommodate its text, and the cell will automatically change size to accommodate the label.
First of all I think that you should add subviews to contentView in subclassed UITableViewCell class initializer method.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubview(titleLabel)
self.contentView.addSubview(descriptionLabel)
}
Secondly, make sure that in your viewDidLoad method (probably in your ViewController) these two lines are added:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableView.automaticDimension
Of course, you should change estimatedRowHeight to accommodate your needs.
One more thing worth mentioning - you can create these constraints easier (using power of SnapKit):
titleLabel.snp.makeConstraints { (make) -> Void in
make.top.left.right.equalTo(contentView)
}
descriptionLabel.snp.makeConstraints { (make) -> Void in
make.top.equalTo(titleLabel.snp.bottom)
make.bottom.left.right.equalTo(contentView)
}