Swift - UITableViewCell height according to UITextView layout constraint - ios

In my table view cell are 3 objects; an imageview, label and a textview.
This is how I set up my textView and its constraints:
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textColor = UIColor.blue
textView.font = UIFont.systemFont(ofSize:15.0)
textView.textAlignment = NSTextAlignment.left
textView.isScrollEnabled = false
**note that self is a UIView that contains the textView. The table view cell's content view will contain this UIView
self.addConstraints([
textView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10.0),
textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: 10.0),
textView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10.0),
textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 20.0)
])
I made isScrollEnabled = false so that the textView's height is calculated according to its content
For my UITableView I did this:
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
With the above code my tableviewcell does not show the correct height. I believe its because my textview height constraint is giving the tableviewcell content view a wrong height.
How can I fix this?

Setting isScrollEnabled = false will not affect calculating textView's height.
In order to resize cell according to textView's content you should add bottom(=) constraint to your textView in addition to top(=) and optional height(>=).

Have you tried using tableView(heightForRowAt:) delegate method?
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return textView.frame.height // Assuming you have a reference to the textview
}

Related

UIScrollVIew not Scrolling Vertically programatically no matter what

I have been trying to scroll the view for a whole day yesterday and I am not able to figure out why it won't scroll. I am not sure what I am doing wrong !!
I have looked at the solutions on stackoverflow:
UIScrollView Scrollable Content Size Ambiguity
How to append a character to a string in Swift?
Right anchor of UIScrollView does not apply
But still, the view doesn't scroll and the scrollview height should be equal to the conrainerView height. But in my case, it stays fixed to the height of the view.
Here is the code repo: https://bitbucket.org/siddharth_shekar/ios_colttestproject/src/master/
Kindly go through and any help would be appreciated. Thanks!!
Here is the code Snippet as well, If you want to go through the constraints and see if there is anything I have added which is not letting the scroll view do its thing !!
I have made changes to the view just one looong text and removed other images, labels, etc to produce the minimal reproducable code.
And I looked at this persons project as well. Their view scrolls!!
https://useyourloaf.com/blog/easier-scrolling-with-layout-guides/
I am just not sure what I am doing differently!!!!
Here is my code for the contentView. It is literally just a textlabel
import Foundation
import UIKit
class RecipeUIView: UIView{
private var recipeTitle: UILabel! = {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = .gray
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
return label
}()
func setupView(currentRecipe: Receipe?){
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
let margin = readableContentGuide
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
}
}
And here is the viewController
import Foundation
import UIKit
class RecipeViewController: UIViewController {
var selectedRecipe: Receipe?
let recipeView: RecipeUIView = {
let view = RecipeUIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
recipeView.setupView(currentRecipe: selectedRecipe)
recipeView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
view.addSubview(scrollView)
scrollView.addSubview(recipeView)
let frameGuide = scrollView.frameLayoutGuide
let contentGuide = scrollView.contentLayoutGuide
// Scroll view layout guides (iOS 11)
NSLayoutConstraint.activate([
frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
frameGuide.topAnchor.constraint(equalTo: view.topAnchor),
frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor),
frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentGuide.leadingAnchor.constraint(equalTo: recipeView.leadingAnchor),
contentGuide.topAnchor.constraint(equalTo: recipeView.topAnchor),
contentGuide.trailingAnchor.constraint(equalTo: recipeView.trailingAnchor),
contentGuide.bottomAnchor.constraint(equalTo: recipeView.bottomAnchor),
contentGuide.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),
])
}
}
And I am still not able to scroll the view. Here is a screenshot of my project output. Still no scroll guide lines on the right!!
UPDATE:: Now the text scrolls, but when I add a UITableView in the UIView the scrolling works but the tableView is not seen in the UiView.
Is it due to the constraints again???
here is the code for the same::
class RecipeUIView: UIView, UITableViewDelegate, UITableViewDataSource{
var currentRecipe: Receipe?
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
tableView.backgroundColor = .green
return tableView
}()
private var recipeTitle: UILabel! = {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = .gray
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("++++ IngrediantsTableViewCell tableview count: \(currentRecipe?.ingredients.count ?? 0)")
return currentRecipe?.ingredients.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("++++ IngrediantsTableViewCell tableview cellForRow ")
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath)
cell.textLabel!.text = "\(currentRecipe?.ingredients[indexPath.row].ingredient ?? "")"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return UITableView.automaticDimension
return 30
}
func setupView(currentRecipe: Receipe?){
let margin = readableContentGuide
self.currentRecipe = currentRecipe
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: recipeTitle.bottomAnchor, constant: 10).isActive = true
tableView.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
tableView.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
tableView.bottomAnchor.constraint(equalTo: margin.bottomAnchor, constant: -20).isActive = true
tableView.reloadData()
}
}
You are missing a constraint...
In your RecipeUIView class, you have this:
func setupView(currentRecipe: Receipe?){
recipeTitle.text = currentRecipe?.dynamicTitle
addSubview(recipeTitle)
let margin = readableContentGuide
// Constraints
recipeTitle.topAnchor.constraint(equalTo: margin.topAnchor, constant: 4).isActive = true
recipeTitle.leadingAnchor.constraint(equalTo: margin.leadingAnchor, constant: 20).isActive = true
recipeTitle.trailingAnchor.constraint(equalTo: margin.trailingAnchor, constant: -20).isActive = true
}
So, you have no constraint controlling the view's Height.
Add this line:
recipeTitle.bottomAnchor.constraint(equalTo: margin.bottomAnchor, constant: -20).isActive = true
And you'll get vertical scrolling.
Two side notes...
First, in ``RecipeViewController`, change your constraints like this:
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
recipeView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
recipeView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
recipeView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
recipeView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor),
recipeView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),
])
There's no real functional difference, but it is more logical and more readable to think in terms of:
I'm constraining the scrollView to the view
I'm constraining the recipeView to the scroll view's .contentLayoutGuide (which determines the "scrollable" size)
I'm constraining the recipeView width the the scroll view's .frameLayoutGuide
Second, giving views contrasting background colors can be very helpful when trying to debug layouts.
For example, if I set background colors like this:
recipeTitle label : cyan
recipeView : yellow
scrollView : orange
It looks like this when running (with your original constraints):
Since the cyan label is a subview of the yellow view, it is obvious that the yellow view height is not correct.
After add the missing bottom constraint, it looks like this:

Using constraints with tableHeaderView of UITableView, tableHeaderView appears on top of cells

I'm creating a custom tableHeaderView using constraints and autolayout. The problem is that my tableHeaderView appears on top of the cells.
Here's my viewDidLoad:
let tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
//Adding subviews and setting constraints
let headerContainer = UIView()
let myView = UIView()
headerContainer.addSubview(myView)
//Setup constraints...
let myLabel = UILabel()
headerContainer.addSubview(myLabel)
//Adding many more views...
tableView.setAndLayoutTableHeaderView(header: headerContainer)
Reading this post, I've copied the suggested extension to UITableView: Is it possible to use AutoLayout with UITableView's tableHeaderView?.
extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
print(header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height) //Always 0???
header.frame.size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
}
The problem is that the header view appears on top of the cells. On printing the size of the headerContainer after layoutIfNeeded and setNeedsLayout, the height is 0...
When you are maintaining the frame yourself, it could be simplified to just this.
extension UITableView {
func setAndLayoutTableHeaderView(header: UIView) {
header.frame.size = header.systemLayoutSizeFitting(size: CGSize(width: self.frame.size.with, height: .greatestFiniteMagnitude))
self.tableHeaderView = header
}
}

Problem with dynamic UITableView cell heights

I want my cells to have dynamic height. I use the below code:
let tableView: UITableView = {
let view = UITableView()
view.register(MyTableViewCell.self, forCellReuseIdentifier: MyTableViewCell.reuseIdentifier)
view.rowHeight = UITableView.automaticDimension
view.estimatedRowHeight = 150
view.separatorStyle = .singleLine
view.isScrollEnabled = true
return view
}()
The cell contains only label that is given one constraint- to be centered inside a cell:
private func setupView() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { maker in
maker.center.equalToSuperview()
}
}
the label's definition:
let titleLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
The label's text is then assigned in cellForRowAt method but in each case returns same hight even though the text is sometimes 4 lines and cell's hight should be stretched.
What is there that I'm missing in the above code? Thanks
The cell's content needs to have autolayout constraints setup in such way that there is constraint connection from the top to the bottom of the cell, for the automatic dimensions to work.
private func setupView() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { maker in
maker.edges.equalToSuperview()
//or add .top, .left, .right, .bottom constraints individually,
//if you need to add .offset() to each of the sides
}
}
You should give the label top , bottom , leading and trailing constraints to make the height dynamic

Multi-line UILabel causing extra padding

I have a collectionView cell that has a label and an imageView. The imageView is 8 points from the top of the cell, the top of the label is 8 points from the bottom of the imageView, and the bottom of the label is 8 points from the bottom of the cell. The label wraps when it gets -10 point away from the right edge of the cell.
The text that goes into the label can span several lines. I use the below function inside the collectionView's sizeForItem to calculate the label's height:
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union([.usesLineFragmentOrigin, .usesFontLeading])
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
The cell expands correctly but the label has extra padding that is added to it and I cannot figure out how to get rid of it.
That is a multi line label size 22 that correctly wraps. I took a picture of it inside the 3D inspector. As you can see there is quite an extra bit of padding on the top and bottom above and below the label text. The label's 8 point spacing below the imageView is correct but the extra padding makes it look like 24 points of spacing.
The odd thing is even when I reduced the label's size to 16 the extra padding was still there.
How can I remove this padding?
The collectionView's sizeForItem where the estimatedLabelHeight function is called:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let profileImageViewHeight: CGFloat = 50 // imageView height is set to 50 inside the cell
let padding: CGFloat = 24 // 8 x 8 x 8 the vertical padding inside the cell between the titleLabel, the imageView, and the cell
// estimatedLabelHeight is called here
let titleLabelHeight = estimatedLabelHeight(text: "some very very long text...", width: view.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
let totalHeightOfCell = titleLabelHeight + profileImageViewHeight + padding
return CGSize(width: view.frame.width, height: totalHeightOfCell)
}
The cell:
class MyCell: UICollectionViewCell{
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 22)
label.textColor = UIColor.black
label.textAlignment = .left
label.sizeToFit()
label.numberOfLines = 0
return label
}()
let profileImageView: UIImageView = {
// created it...
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(profileImageView)
addSubview(titleLabel)
profileImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
profileImageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
titleLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 8).isActive = true
titleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true
// I tried to set the label's height using the below but the same padding issue occured
// let titleLabelHeight = estimatedLabelHeight(text: titleLabel.text!, width: self.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
// titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: titleLabelHeight).isActive = true
}
}
The problem was due to my own carelessness. #CSmith tip of measuring up the height in sizeForItem and the cell's frame is what helped me narrow it down.
Inside the project inside the collectionView cell I had the the imageView's bottom anchor set to 8 instead of -8 and inside the collectionView's sizeForItem I had the total height of 24. There was a conflict because since I had a 8 inside the cell it should've been 16 inside the collectionView and that mismatch was somehow stretching out the label. Once I corrected it and changed the imageView's bottom anchor -8 everything matched up and the label's padding issue was resolved.

Animate UITableViewCell height

I have a container view inside a UITableViewCell which is attached to the 4 edges of the contentview with a padding. I can resize the containerView by pinching changing the height and padding. All the constraints update correctly but the contentView height doesn't change. Here are the constraints of the containerView.
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerViewLeadingAnchor =
containerView.leadingAnchor .constraint(equalTo: contentView.leadingAnchor, constant: 10)
containerViewTopAnchor =
containerView.topAnchor .constraint(equalTo: contentView.topAnchor, constant: 10)
containerViewBottomAnchor =
containerView.bottomAnchor .constraint(equalTo: contentView.bottomAnchor, constant: -10)
containerViewHeightAnchor =
containerView.heightAnchor .constraint(equalToConstant: 146)
containerViewWidthAnchor =
containerView.widthAnchor .constraint(equalToConstant: UIScreen.main.bounds.width - 20)
containerViewLeadingAnchor.isActive = true
containerViewTopAnchor.isActive = true
containerViewBottomAnchor.isActive = true
containerViewHeightAnchor.isActive = true
containerViewWidthAnchor.isActive = true
And here is how i update them inside the UITableViewCell
containerViewLeadingAnchor.constant = containerPaddingForComfortable - (containerPaddingForComfortable * scaleRatio)
containerViewTopAnchor.constant = containerPaddingForComfortable - (containerPaddingForComfortable * scaleRatio)
containerViewWidthAnchor.constant = (UIScreen.main.bounds.width - 20) + (containerPaddingForComfortable*2 * scaleRatio)
containerViewBottomAnchor.constant = -containerPaddingForComfortable + (containerPaddingForComfortable * scaleRatio)
containerView.layer.cornerRadius = containerCornerRadius - (containerCornerRadius * scaleRatio)
self.layoutIfNeeded()
It is not enough to simply modify view frame or constraints to update the size of content view.
In table view the cell content view is controlled by cell which is controlled by table view or rather it's data source or delegate. In your case you need to have cell height set to automatic dimensions so constraints are used internally to determine cell size. Once that is done you will need to call beginUpdates() and endUpdates() on the table view. So something like:
func resizeMyCell(_ cell: MyCell, to height: CGFloat) {
cell.heightConstraint.constant = height
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
Although it might seem strange the two calls will invalidate sizes and refresh table view cell sizes and positions. By the way changing cell size usually means also moving rest of the cells beneath the resized one.

Resources