row height is not automatically updated - ios

EDITED:
This is my custom cell class. It has a TextField and a TextView. Whatever I do I can't get the row height updated automatically. I know I can do it manually using heightForRowAt but I don't want to do that.
class customCell: UITableViewCell, UITextViewDelegate{
var didSetupConstraints = false
var titleField : UITextField = {
var textField = UITextField()
textField.placeholder = " Subject (optional)"
textField.backgroundColor = UIColor.lightGray
textField.translatesAutoresizingMaskIntoConstraints = false
textField.layer.cornerRadius = 3
textField.clipsToBounds = true
return textField
}()
var messageView : UITextView = {
var textView = UITextView()
textView.text = "Add your email here"
textView.translatesAutoresizingMaskIntoConstraints = false
textView.backgroundColor = UIColor.red
return UITextView()
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubview(titleField)
self.contentView.addSubview(messageView)
messageView.delegate = self
addConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addConstraints(){
contentView.addConstraints([titleField.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 23),titleField.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -18),titleField.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 18) ])
titleField.addConstraint(NSLayoutConstraint(item: titleField, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50))
contentView.addConstraints([messageView.topAnchor.constraint(equalTo: titleField.bottomAnchor, constant: 11),messageView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -18),messageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 18), messageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5)])
messageView.addConstraint(NSLayoutConstraint(item: messageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 100))
}
override func layoutSubviews() {
super.layoutSubviews()
contentView.setNeedsLayout()
contentView.layoutIfNeeded()
}
override func updateConstraints() {
if !didSetupConstraints {
addConstraints()
didSetupConstraints = true
}
super.updateConstraints()
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == UIColor.lightGray {
textView.text = nil
textView.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = "Add your email here"
textView.textColor = UIColor.lightGray
}
}
}
I have already seen this question and from what I have understood the things I need to do are:
Add tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableViewAutomaticDimension which I have done in tableViewController
Add a bottom and top constraint: I have added a topAnchor to my TextField + a constraint between my TextField and TextView + a constraint between my TextView's bottomAnchor and the contentView bottomAnchor
I have added my constraints code into my updateConstraints() method.
Not sure if I need to do anything else, but I've done all three but it still doesn't work. I'm guessing that maybe my bottom/top constraints are not set up correctly. The current result that I get is (The textView isn't visible at all :(( )
yet what I expect to get is:
EDIT 2
See image:
After all the fixes, the only problem I have now is that the empty cells don't have the default size of 44, is it that the tableView is trying to be smart and adjusts the row height based on the last cell height?

A few things:
updateConstraints can be called multiple times by the system, so use a flag to only add your constraints the first time.
messageView.topAnchor.constraint(equalTo: titleField.topAnchor, constant: 11) should be messageView.topAnchor.constraint(equalTo: titleField.bottomAnchor, constant: 11)
Try giving your messageView a height.
As #Honey pointed out, textView was not returned in the initialization of messageView.
About empty cell heights, if you don't want empty cells at all, just do tableView.tableFooterView = UIView() to get rid of them. It's probably the table view being smart about cell heights, like you said.

Related

Self sizing cell with multiple Stack Views

I've looked all over the forum and attempted all the solutions and thus far nothing has worked. I noticed my UIImageView was overlaying multiple cells, meaning the celll did not automatically adjust its height. Here is the constraint i found in the console it complained about.
"<NSLayoutConstraint:0x600001970f50 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7f86a4813dd0.height == 44 (active)>"
In my tableViewController I have the follow
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 300
Here is my entire cell that should self size.
import UIKit
class UserConnectionCell: UITableViewCell {
fileprivate let leftImageView: UIImageView = {
let uiImageView = UIImageView()
uiImageView.translatesAutoresizingMaskIntoConstraints = false
return uiImageView
}()
fileprivate let leftLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let middleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont(name: "Ariel", size: 10)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let rightImageView: UIImageView = {
let uiImageView = UIImageView()
uiImageView.translatesAutoresizingMaskIntoConstraints = false
return uiImageView
}()
fileprivate let rightLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let stackViewLeft: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
fileprivate let stackViewRight: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
fileprivate let stackViewMain: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
//
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier:reuseIdentifier)
stackViewLeft.addArrangedSubview(leftImageView)
stackViewLeft.addArrangedSubview(leftLabel)
stackViewRight.addArrangedSubview(rightImageView)
stackViewRight.addArrangedSubview(rightLabel)
stackViewMain.addArrangedSubview(stackViewLeft)
stackViewMain.addArrangedSubview(middleLabel)
stackViewMain.addArrangedSubview(stackViewRight)
contentView.addSubview(stackViewMain)
}
// called when trying to layout subviews.
override func layoutSubviews() {
super.layoutSubviews()
stackViewLeft.addConstraint(NSLayoutConstraint(item: leftImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 100))
stackViewLeft.addConstraint(NSLayoutConstraint(item: leftImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 100))
stackViewRight.addConstraint(NSLayoutConstraint(item: rightImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 100))
stackViewRight.addConstraint(NSLayoutConstraint(item: rightImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 100))
NSLayoutConstraint.activate(
[stackViewMain.topAnchor.constraint(equalTo: contentView.topAnchor,constant: 0),
stackViewMain.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 0),
stackViewMain.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: 0),
stackViewMain.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var viewModel : UserConnectionViewModel? {
didSet {
// move this to the view model
if let profileUrl = viewModel?.leftImageUrl {
leftImageView.loadImageFromURL(url: profileUrl)
} else {
leftImageView.image = UIImage(named: "defaultprofile")
}
if let profileUrl = viewModel?.rightImageUrl {
rightImageView.loadImageFromURL(url: profileUrl)
} else {
rightImageView.image = UIImage(named: "defaultprofile")
}
leftLabel.text = viewModel?.leftLabel
middleLabel.text = viewModel?.middleLabel
rightLabel.text = viewModel?.rightlabel
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.autoresizingMask = .flexibleHeight
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Any ideas for why the cell is not self sizing?
First, a cell's contentView is a "special" view with properties integral to the table view's operation.
So, do not do this:
self.contentView.autoresizingMask = .flexibleHeight
Second, layoutSubviews() can be (and usually is) called multiple times during the lifecycle of a cell / view. Your constraint setup should be done in init:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier:reuseIdentifier)
stackViewLeft.addArrangedSubview(leftImageView)
stackViewLeft.addArrangedSubview(leftLabel)
stackViewRight.addArrangedSubview(rightImageView)
stackViewRight.addArrangedSubview(rightLabel)
stackViewMain.addArrangedSubview(stackViewLeft)
stackViewMain.addArrangedSubview(middleLabel)
stackViewMain.addArrangedSubview(stackViewRight)
contentView.addSubview(stackViewMain)
NSLayoutConstraint.activate([
// constrain main stack view to all 4 sides of contentView
stackViewMain.topAnchor.constraint(equalTo: contentView.topAnchor,constant: 0),
stackViewMain.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 0),
stackViewMain.trailingAnchor.constraint(equalTo: contentView.trailingAnchor,constant: 0),
stackViewMain.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
// constrain left image view Width: 100-pts,
// Height equal to Width (1:1 ratio)
leftImageView.widthAnchor.constraint(equalToConstant: 100.0),
leftImageView.heightAnchor.constraint(equalTo: leftImageView.widthAnchor),
// constrain right image view Width: 100-pts,
// Height equal to Width (1:1 ratio)
rightImageView.widthAnchor.constraint(equalToConstant: 100.0),
rightImageView.heightAnchor.constraint(equalTo: rightImageView.widthAnchor),
])
}
So... replace your init with the above code and completely remove both your awakeFromNib() and layoutSubviews() funcs.
You should get this:

Unable to Anchor UIImageVIew in UICollectionViewCell (programmatically)

Working on specing out a view in Playground and can't seem to figure out why UIIMageView is being placed in the center of a UICollectionViewCell.
Relevant Code:
class BookCell: UICollectionViewCell {
static let identifier = "bookCell"
override init(frame: CGRect) {
super.init(frame: .zero)
self.layer.cornerRadius = 12
self.backgroundColor = .brown
addAllSubviews()
addAllConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var cover: UIImageView = {
let imageview = UIImageView()
imageview.translatesAutoresizingMaskIntoConstraints = false
var largeImage = UIImage(named: "medium.jpg")
imageview.image = largeImage
imageview.contentMode = .scaleAspectFit
//imageview.contentMode = .scaleToFill
return imageview
}()
func coverConstraints(){
NSLayoutConstraint.activate([
cover.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0),
/**widthConstraint*/
NSLayoutConstraint(item: cover,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 1.0, constant: 0.0),
/**heightConstraint*/
NSLayoutConstraint(item: cover,
attribute: .height,
relatedBy: .equal,
toItem: self,
attribute: .height,
multiplier: 0.75, constant: 0.0)
])
}
let wordLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "test"
return label
}()
func wordLabelConstraints() {
NSLayoutConstraint.activate([
wordLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
wordLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
wordLabel.topAnchor.constraint(equalTo: cover.bottomAnchor, constant: 2)
])
}
// MARK: - Add Subviews
func addAllSubviews() {
self.addSubview(cover)
self.addSubview(wordLabel)
}
// MARK: - SubViews Constraints
func addAllConstraints() {
coverConstraints()
wordLabelConstraints()
}
}
BookCell is then used in a UICollectionViewController like so:
class ViewController: UIViewController {
fileprivate let collectionView: UICollectionView = {
let layout = ColumnFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .blue
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.register(BookCell.self, forCellWithReuseIdentifier: BookCell.identifier)
self.collectionView.alwaysBounceVertical = true
self.collectionView.backgroundColor = .yellow
}
}
Result:
I noticed that using scaleToFill instead of scaleAspectFit results in image covering the entire width of the cell. The result (see image below) fits what I am aiming for but ... see question below
Question:
Is using scaleToFill the only way to pin an image to the edges (leading and trailing) of UICollectionViewCell. If so, why is this?
I also tried adding the UIImageView to a UIStackView and I believe I got the same results.
Please note that I am not interested in doing this via Storyboard.
Thank you for providing feedback
There is one more option: .scaleAspectFill but may be cropped your image's content.
The option to scale the content to fill the size of the view.
Some portion of the content may be clipped to fill the view’s bounds.
Think about getting the image size ratio (width/height), you having a fixed width based on superview, and height will be based on the image Ratio.

Async height change for UITableViewCell

In one of my projects, I need to change the height of UIImageView in UITableViewCell according to image size, but the problem is that sometimes I have to do this after the cell is already shown.
So, my current solution works like a charm if I know all the image sizes beforehand, but if I'm trying to calculate this with some delay – it's completely broken (especially with scrolling but it's broken even without it).
I made the example project to illustrate this. There is no async downloading, but I'm trying to dynamically change the height of UIImageView after some delay (1s). The height depends on UIImageView, so every next UIImageView should be slightly higher (10 pixels) than previous one. Also, I have a UILabel, constrained to UIImageView.
It looks like that (UIImageViews are the red ones)
If I'm trying to do this async, it looks like this, all the UILabels are really broken here.
and this is one after the scroll (async too):
What am I doing wrong here? I've read several threads about dynamic heights, but none of the solutions worked for me yet.
My code is fairly simple:
func addTableView() {
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .black
tableView.register(DynamicCell.self, forCellReuseIdentifier: "dynamicCell")
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dynamicCell", for: indexPath) as! DynamicCell
cell.message = messageArray[indexPath.row]
cell.backgroundColor = .clear
cell.selectionStyle = .none
cell.buildCell()
return cell
}
DynamicCell.swift (delegate is doing nothing right now):
var backView: UIView!
var label: UILabel!
var picView: UIImageView!
var message: DMessage?
var picViewHeight: NSLayoutConstraint!
var delegate: RefreshCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backView = UIView()
backView.translatesAutoresizingMaskIntoConstraints = false
backView.backgroundColor = .white
backView.clipsToBounds = true
backView.layer.cornerRadius = 8.0
self.addSubview(backView)
label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .left
label.textColor = .black
label.numberOfLines = 0
backView.addSubview(label)
picView = UIImageView()
picView.translatesAutoresizingMaskIntoConstraints = false
picView.clipsToBounds = true
picView.backgroundColor = .red
backView.addSubview(picView)
addMainConstraints()
}
func addMainConstraints() {
backView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
backView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -32).isActive = true
backView.topAnchor.constraint(equalTo: self.topAnchor, constant: 4).isActive = true
backView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4).isActive = true
picView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0).isActive = true
picView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 0).isActive = true
picView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: 0).isActive = true
label.topAnchor.constraint(equalTo: picView.bottomAnchor, constant: 0).isActive = true
label.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 8).isActive = true
label.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -8).isActive = true
label.bottomAnchor.constraint(equalTo: backView.bottomAnchor, constant: -4).isActive = true
picViewHeight = NSLayoutConstraint(item: picView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
picViewHeight.priority = UILayoutPriority(999)
picViewHeight.isActive = true
}
override func prepareForReuse() {
picViewHeight.constant = 0
//picViewHeight.constant = 0
}
func buildCell() {
guard let message = message else {return}
label.attributedText = NSAttributedString(string: message.text)
changeHeightWithDelay()
//changeHeightWithoutDelay()
}
func changeHeightWithoutDelay() {
if let nh = self.message?.imageHeight {
self.picViewHeight.constant = nh
self.delegate?.refreshCell(cell: self)
}
}
func changeHeightWithDelay() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if let nh = self.message?.imageHeight {
self.picViewHeight.constant = nh
self.delegate?.refreshCell(cell: self)
}
}
}
putting this as an answer.
one thing I noticed, when you are playing around with cell, it's always better to use the contentView instead of directly using self. ie self.contentView.addSubview(). what does refreshcell function do? have you tried marking it as needsSetDisplay so in the next draw cycle it will be updated? have you tried calling layoutIfNeeded?
To explain a bit further, your view has already been 'rendered' the moment you want to change the height/width of your view you need to inform it that there's an update. this happens when you mark the view as setNeedsDisplay and in the next render cycle it will be updated
more info on apple's documentation here
You can use this method or the setNeedsDisplay(_:) to notify the system that your view’s contents need to be redrawn. This method makes a note of the request and returns immediately. The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.

Preventing Auto Layout from stretching custom view

I've created a custom view in swift & am trying to get it to display appropriately. It is essentially a material card, with the ability to expand the view by pressing the more button. My issue comes in when specifying the bottom constraint. It is required, but setting it stretches my custom view.
I have implemented this already in android & I guess I'm trying to find the analog to android:height='wrap_content'. I've tried setting the aspect ratio constraint, which works to keep my view at the proper size, but prevents the custom view from expanding when its subviews change. Additionally I've tried using the lessThanOrEqualTo constraint, but that is too ambiguous to satisfy the bottom constraint.
This is what my UIExpandableCard view looks like:
import Foundation
import MaterialComponents
#IBDesignable
public class UIExpandableCard: UIView {
// attributes
#IBInspectable var overlineText: String? {
didSet {
overlineLabel.text = overlineText?.uppercased()
}
}
#IBInspectable var headlineText: String? {
didSet {
headlineLabel.text = headlineText
}
}
#IBInspectable var bodyText: String? {
didSet {
bodyLabel.text = bodyText
}
}
#IBInspectable var logoImage: UIImage? {
didSet {
logoImageView.image = logoImage
}
}
var cardView: MDCCard!
var overlineLabel: UILabel!
var headlineLabel: UILabel!
var bodyLabel: UILabel!
var moreButton: UIButton!
var logoImageView: UIImageView!
var isCardExpanded = false
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.translatesAutoresizingMaskIntoConstraints = false
setupViews()
setupConstraints()
}
private func setupViews() {
self.clipsToBounds = true
cardView = MDCCard(frame: CGRect.zero)
cardView.translatesAutoresizingMaskIntoConstraints = false
cardView.isInteractable = false
self.addSubview(cardView)
overlineLabel = UILabel(frame: CGRect.zero)
overlineLabel.translatesAutoresizingMaskIntoConstraints = false
overlineLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
overlineLabel.text = overlineText?.uppercased()
self.addSubview(overlineLabel)
headlineLabel = UILabel(frame: CGRect.zero)
headlineLabel.translatesAutoresizingMaskIntoConstraints = false
headlineLabel.font = UIFont.preferredFont(forTextStyle: .title1)
headlineLabel.text = headlineText
self.addSubview(headlineLabel)
bodyLabel = UILabel(frame: CGRect.zero)
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
bodyLabel.numberOfLines = 1
bodyLabel.text = bodyText
self.addSubview(bodyLabel)
logoImageView = UIImageView(image: logoImage)
logoImageView.translatesAutoresizingMaskIntoConstraints = false
logoImageView.contentMode = .scaleAspectFit
self.addSubview(logoImageView)
moreButton = UIButton(type: .roundedRect)
moreButton.isUserInteractionEnabled = true
moreButton.translatesAutoresizingMaskIntoConstraints = false
moreButton.setTitle("More", for: .normal)
moreButton.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
self.addSubview(moreButton)
}
#objc func buttonClicked(_ sender: UIButton) {
if !isCardExpanded {
moreButton.setTitle("Less", for: .normal)
bodyLabel.numberOfLines = 0
} else {
moreButton.setTitle("More", for: .normal)
bodyLabel.numberOfLines = 1
}
isCardExpanded = !isCardExpanded
}
private func setupConstraints() {
NSLayoutConstraint.activate([
cardView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
cardView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8),
cardView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
cardView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -8),
overlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
overlineLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),
headlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
headlineLabel.topAnchor.constraint(equalTo: overlineLabel.bottomAnchor, constant: 8),
NSLayoutConstraint(item: logoImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 48),
NSLayoutConstraint(item: logoImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 48),
logoImageView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16),
logoImageView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),
bodyLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
bodyLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 16),
bodyLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
moreButton.topAnchor.constraint(greaterThanOrEqualTo: bodyLabel.bottomAnchor, constant: 8),
moreButton.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
moreButton.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -8)
])
}
}
Basically I want something like the left. However, I'm getting the right, where one of the views (in blue) is being stretched to fill the constraint.
left right
I'm relatively new to iOS, but have experience with android, so any explanations relating to that would be extra helpful.
Thanks.
So I found a solution that appears to do what I need. I'm still trying to wrap my head around what I was actually trying to achieve. Basically what I wanted was my view height to be defined by its subviews & their constraints, rather than having to specify it in a constraint. At the same time I needed to satisfy the height constraint in my interface.
My solution was as follows: add a low priority height constraint of 0 to the the card view in my interface. This satisfies the requirement for a height in the scene, while also allowing my view to expand & contract without being stretched.
... a low priority constraint of height zero for the view as a whole. The low priority constraint will try to shrink the assembly, while the other constraints stop it from shrinking so far that it clips its subviews.
I found this solution on another stack overflow question.
Following Joshua's answer, with this I prevented a custom view from stretching:
let width = widthAnchor.constraint(equalToConstant: 0)
width.priority = .defaultLow
width.isActive = true
You can't do that with lessThanOrEqualTo.Although i can't fully understand you're question but hope this approach help.
first of all define a flow constraint like:
var heightConstraint: NSLayoutConstraint?
and replace it with bottom constraint in setupConstraint:
private func setupConstraints() {
heightConstraint = cardView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([
cardView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
cardView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8),
cardView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
heightConstraint,
overlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
overlineLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),
.
.
.
and finally when press the button:
private func expandViewWithAnimation(_ isExpand: Bool) {
UIView.animate(withDuration: 0.8) {
self.heightConstraint?.constant = isExpand ? 80:0
self.layoutIfNeeded()
}
}
#objc func buttonClicked(_ sender: UIButton) {
if !isCardExpanded {
moreButton.setTitle("Less", for: .normal)
bodyLabel.numberOfLines = 0
} else {
moreButton.setTitle("More", for: .normal)
bodyLabel.numberOfLines = 1
}
expandViewWithAnimation(isCardExpanded)
isCardExpanded = !isCardExpanded
}
i can't comment on your post because don't have enough reputation :)

UITableView with variable cell height: Working in IB but not programmatically

TL;DR
My programmatically created table view cells are not resizing according to the intrinsic content height of their custom views, even though I am using UITableViewAutomaticDimension and setting both the top and bottom constraints.
The problem probably lies in my implementation of the UITableViewCell subclass. See the code below under Doesn't work programmatically > Code > MyCustomCell.swift.
Goal
I'm trying to make a suggestion bar for a custom Mongolian keyboard. Mongolian is written vertically. In Android it looks like this:
Progress
I've learned that I should use a UITableView with variable cell heights, which is available starting with iOS 8. This requires using auto layout and telling the table view to use automatic dimensions for the cell heights.
Some things I've had to learn along the way are represented in my recent SO questions and answers:
How to make a custom table view cell
Getting variable height to work with in a table view with a standard UILabel
Getting intrinsic content size to work for a custom view
Using a programmatically created UITableViewCell
Set constraints programmatically
So I have come to the point where I have the vertical labels that support intrinsic content size. These labels go in my custom table view cells. And as described in the next section, they work when I do it in the storyboard, but not when I create everything programmatically.
Works in IB
In order to isolate the problem I created two basic projects: one for where I use the storyboard and one where I do everything programmatically. The storyboard project works. As can be seen in the following image, each table view cell resizes to match the height of custom vertical label.
In IB
I set constraints to pin the top and bottom as well as centering the label.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
import UIKit
class MyCustomCell: UITableViewCell {
#IBOutlet weak var myCellLabel: UIMongolSingleLineLabel!
}
Doesn't work programmatically
Since I want the suggestion bar to be a part of the final keyboard, I need to be able to create it programmatically. However, when I try to recreate the above example project programmatically, it isn't working. I get the following result.
The cell heights are not resizing and the custom vertical labels are overlapping each other.
I also get the following error:
Warning once only: Detected a case where constraints ambiguously
suggest a height of zero for a tableview cell's content view. We're
considering the collapse unintentional and using standard height
instead.
This error has been brought up before multiple times on Stack Overflow:
iOS8 - constraints ambiguously suggest a height of zero
Detected a case where constraints ambiguously suggest a height of zero
custom UITableviewcell height not set correctly
ios 8 (UITableViewCell) : Constraints ambiguously suggest a height of zero for a tableview cell's content view
However, the problem for most of those people is that they were not setting both a top and bottom pin constraint. I am, or at least I think I am, as is shown in my code below.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Suggestion bar
tableView.frame = CGRect(x: 0, y: 20, width: view.bounds.width, height: view.bounds.height)
tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdentifier)
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
I think the problem is probably in here since this is the main difference from the IB project.
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
Supplemental Code
I'll also include the code for the custom vertical label that I used in both projects above, but since the IB project works, I don't think the main problem is here.
import UIKit
#IBDesignable
class UIMongolSingleLineLabel: UIView {
private let textLayer = LabelTextLayer()
var useMirroredFont = false
// MARK: Primary input value
#IBInspectable var text: String = "A" {
didSet {
textLayer.displayString = text
updateTextLayerFrame()
}
}
#IBInspectable var fontSize: CGFloat = 17 {
didSet {
updateTextLayerFrame()
}
}
#IBInspectable var centerText: Bool = true {
didSet {
updateTextLayerFrame()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
// Text layer
textLayer.backgroundColor = UIColor.yellowColor().CGColor
textLayer.useMirroredFont = useMirroredFont
textLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(textLayer)
}
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
func updateTextLayerFrame() {
let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ]
let attrString = NSMutableAttributedString(string: textLayer.displayString, attributes: myAttribute )
let size = dimensionsForAttributedString(attrString)
// This is the frame for the soon-to-be rotated layer
var x: CGFloat = 0
var y: CGFloat = 0
if layer.bounds.width > size.height {
x = (layer.bounds.width - size.height) / 2
}
if centerText {
y = (layer.bounds.height - size.width) / 2
}
textLayer.frame = CGRect(x: x, y: y, width: size.height, height: size.width)
textLayer.string = attrString
invalidateIntrinsicContentSize()
}
func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var width: CGFloat = 0
let line: CTLineRef = CTLineCreateWithAttributedString(attrString)
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil))
// make width an even integer for better graphics rendering
width = ceil(width)
if Int(width)%2 == 1 {
width += 1.0
}
return CGSize(width: width, height: ceil(ascent+descent))
}
}
// MARK: - Key Text Layer Class
class LabelTextLayer: CATextLayer {
// set this to false if not using a mirrored font
var useMirroredFont = true
var displayString = ""
override func drawInContext(ctx: CGContext) {
// A frame is passed in, in which the frame size is already rotated at the center but the content is not.
CGContextSaveGState(ctx)
if useMirroredFont {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextScaleCTM(ctx, 1.0, -1.0)
} else {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextTranslateCTM(ctx, 0, -self.bounds.width)
}
super.drawInContext(ctx)
CGContextRestoreGState(ctx)
}
}
Update
The entire code for the project is all here, so if anyone is interested enough to try it out, just make a new project and cut and paste the code above into the following three files:
ViewController.swift
MyCustomCell.swift
UIMongolSingleLineLabel.swift
The error is pretty trivial:
Instead of
self.addSubview(myCellLabel)
use
self.contentView.addSubview(myCellLabel)
Also, I would replace
// pin top
NSLayoutConstraint(...).active = true
// pin bottom
NSLayoutConstraint(...).active = true
// center horizontal
NSLayoutConstraint(...).active = true
with
let topConstraint = NSLayoutConstraint(...)
let bottomConstraint = NSLayoutConstraint(...)
let centerConstraint = NSLayoutConstraint(...)
self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
which is more explicit (you have to specify the constraint owner) and thus safer.
The problem is that when calling active = true on a constraint, the layout system has to decide to which view it should add the constraints. In your case, because the first common ancestor of contentView and myCellLabel is your UITableViewCell, they were added to your UITableViewCell, so they were not actually constraining the contentView (constraints were between siblings not between superview-subview).
Your code actually triggered a console warning:
Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a tableview cell's content view. We're considering the collapse unintentional and using standard height instead.
Which made me to look immediately at the way the constraints are created for your label.
I have tested your code and found the issue was in setting constraints please use below code part for setting constants in your "MyCustomCell.swift" file setup() function
let topConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0)
let centerConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)
self.addConstraints([centerConstraint, topConstraint, bottomConstraint])
Also set clips to bound property to your cell lable in "viewcontroller.swift"
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
cell.myCellLabel.clipsToBounds=true
return cell
}
For your ease I have uploaded my sample code on GitHub Dynamic Height Sample
Output is looking like this now
The problem seems to come from the vertical constraints in the cell
By putting them relative to self instead of self.contentView in MyCustomCell you can fix your problem
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
the full class would be:
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
The thing you are missing is this function:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightValue
}
Im not so sure what you should do exactly, but by the fact that you know your labels you should be able to return an exact height value for each cell in this method
I think you are missing to set constraints for tableView with superview. And try to increase estimated row height also.

Resources