Height of UICollectionViewCell with 2 multiline UILabels / self sizing collection view cells - ios

I have a UICollectionViewCell prototype which should look like this:
On the left side, there is a UIImageView which I centered vertically in and pinned it to the start of the UICollectionViewCell.
Next comes a title UILabel which I pinned to the top and end of the UICollectionViewCell. I also pinned it to the end of the UIImageView.
Below comes a detail UILabel which I pinned to the bottom of the title, the end of the UICollectionViewCell and constraint it to have the same width as the title.
Below that comes a description UILabel which I pinned to the bottom of the detail, the end of the UICollectionViewCell and the bottom of the UICollectionViewCell. It also has the same width as the title.
What I want to achieve
The title and description labels have 0 number of lines (multiline) because I don't know how many lines the labels will have at runtime.
Via my custom UICollectionViewLayout I provide the target width for the cell. Now I need to figure out, how height the cell must be for the given width.
I have a non visible UICollectionViewCell which I think I can use to calculate the sizes by setting the model from my datasource. But I have a problem.
The Problem
After I set the text of the labels in my "size calculation cell", what do I do with the width and what else do I have to do so I can let the auto layout system calculate a proper height for the cell which I can then return to my UICollectionViewLayout
Or in other words: how can I tell the cell to use the new width to calculate its height.

Add this extension to the String:
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
And then do this:
let heightOfLabel = self.string.height(withConstrainedWidth: labelWidth, font: UIFont(name: "HelveticaNeue", size: 15)!)
You will get the height of the label with that font and that width. After that you can sum up the label heights and give it to the cell.

Okay it seems that I misunderstood how to use sizeThatFits. I now went with a different approach, though. This is what I did to get my self-sizing cells:
I put the three labels in a vertical UIStackView
I put the UIImageView and the UIStackView in a horizontal UIStackView
I pinned to parent stack view to left, right, top and bottom of the cell where the bottom constraint is >= 0 instead of just = 0
I use a protocol in my UICollectionViewLayout to ask for the height of a given width
In my UICollectionViewController, I implement the protocol and where I register my cells, I also instantiate one of the cells which I use for measuring in the protocol function
In the protocol, I first get the model object, then I hand down the model to my measuring cell which I keep in an instance variable in my view controller
I then call getHeight(forWidth) on my cell. In this function, I set the cells frame.size.height to a very large number. Then I use AutoLayout to calculate the actual height (setNeedsDisplay(), layoutIfNeeded())
The height I need is the height of the parent stack view plus the margings
I hope this helps somebody like me in the future. I also wrote a small blog post with some code samples of what I'm doing.

Related

How to achieve this layout in Interface Builder?

I'm using a static Table View Controller and defining the UI in Interface Builder.
I want all of the UILabels to be the same width so that the the left side of the UITextFields line up.
If I force a width, then they often overdraw each other.
I was thinking of modifying the UILabel width programmatically, based on the width of "Safe Message" within the controller, but I couldn't get it to re-layout.
let width = labelSafeMessage.frame.size.width;
labelName.frame.size.width = labelSafeMessage.frame.size.width;
labelName.sizeToFit();
labelMessage.frame.size.width = labelSafeMessage.frame.size.width;
labelMessage.sizeToFit();
You can do this using only Interface Build and you do not need to force labels to have the same size.The only thing that you need to do is set leading spacing of UITextFields to superview and they will be aligned to left side.
you could avoid re-layout by calculating the width before the tableView displays data.
get the largest string in the set, and then use this
var str = "Hello, playground"
let constraintRect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: DEFINED_HEIGHT)
let boundingBox = (str as? NSString)?.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: FONT], context: nil)
boundingBox.width //width for the labels
Just replace the DEFINED_HEIGHT with the height for the label which would be the height of the row minus the top and bottom constraints that hold it.
and the FONT, for the font used in the label.
Instead of using a UITableView you could use a vertical UIStackView who will contains UIViews.
Set your UILabels constraints to Align Leading edges and Align Trailing edges and set the Content Hugging Priority and Content Compression Resistance Priority as well as the label don't be cropped and fit correctly.
Set the UITextFields constraints to Align Leading edges and Align Trailing edges to each other and that will do the trick.

Self sizing tableview

First of all this is not a question about how to automatically size the cells inside the tableview, moreover how to automatically resize the entire tableview.
So I have a scrollview which has a tableview and 2 other views as its subviews. The tableview's cells already automatically resize itself, again this question is not about the individual cells. However, the tableview does not resize at all.
What I have done:
1) Set up the tableview to have a top, bottom, leading and trailing constraint
2) Set the cells up to have auto layout enabled
3) * I do not know the cell size at build time
4) I have disabled scrolling mode on tableview
So long story short, how can I go along to get the tableview to resize itself?
Edit
The cells contain a label which can have various lines of text, so therefore the cells, which use auto layout, should then determine the height of the table view.
The following images show how the view is set up:
As you can see the tableview is only a small part of the view and since the scrollview from the tableview is deactivated there should, and aren't, any scrolling problems.
EDIT 2
This is actually how it should end however, i am calculating this on my own and everytime I want to make a small change to the cells the whole code, which calculates the height of the cell, needs to be rewritten and it is quite difficult for me to get the height just right.
Edit 3
Until now I had a height constraint on the tableview which I calculated manually, however removing this constraint and trying to let auto layout handle the tableview height size creates the following error:
Scroll View
Need constraint for: Y position or height
I can conclude therefore that the tableview does not know how to automatically calculate the height based on its cells with autolayout.
You don't need to create a height constraint or set frame whatsoever. Create a subclass of UITableView and recalculate its intrinsicContentSize every time its contentSize changes aka new data added or removed. Here is all you needed:
class SelfSizingTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(.infinity, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
You can change your UITableView's frame by using tableview.frame = CGRect(x: <some_x>, y: <some_y>, width: <some_width>, height: <some_height>)
If your UITableViewCells use auto layout then they should resize when the UITableView's frame changes.

Dynamic height for UITableView ( Not Cell )

I'm trying to have a UITableView that lists all the different HomeKit devices a user has available.
Obviously there is no way to know how many devices they have, so I need to have the UITableView's height in the storyboard change.
I've tried this, which I call in the viewDidLoad() function:
func adjustHeightOfTableView() {
//getting the height of the tableview
var tableHeight = self.tableView.contentSize.height
//the height of the content inside the view
var maxHeight = self.tableView.superview?.frame.size.height
//if the height of the content is bigger then the height of the tableview
if (maxHeight! > tableHeight) {
tableHeight = maxHeight!
//set the tableview height to be the content height
}
//trying to reload the tableview height?
self.view.layoutIfNeeded()
self.tableView.reloadData()
}
I am trying to have some UI Elements under the tableview, and I want them to be a set space from the bottom of the tableview, but also have the tableview be the height that it needs to be, for whatever amount of cells there is.
But it's just not working.
If I'm doing anything wrong, or if anyone knows how to make this work, please let me know.
Thanks!
Note: For this approach you need to have static cell height or figure out a way to know before hand whats the total contentsize height
Assuming you are using constraints, create following constraints on your UITableView (apart from leading and trailing!)
Add a height constraint with a priority of 750 and a bottom spacing constraint of 0 to your super view that will be >= 0 and have a priority of 1000. Create outlet for this height constraint that you created in your UIViewController
Now,
func adjustHeightOfTableView() {
//set the height to be equal to the number of elements multiplied by the height of each cell.
//or use some logic that allows you to know what content size or space the cells will occupy!
tableViewHeightConstraint.constant = dataArray.count * rowHeight
view.layoutIfNeeded()
}
Now if your UITableView height is less than super view, no problems! But if it is greater than screen bounds, it will break the height constraint and become full screen and display the content normally as you expect a UITableView to!
Edit:
Even if you are using UIAutomaticRowDimensions what you can do is add constraints programmatically to your UITableView. i.e
Of course all your other views will still have a bottom constraint to your UITableView.
Create a UITableView in your storyboard with normal leading, trailing, top and bottom to the super view. Fetch the data. Get the contentSize for your UITableView and then remove the bottom constraint. Now add a height constraint that will be the minimum value of your UIScreen.main().bounds.size.height and contentSize.
you can use Automatic Dimensions if you are using autolayouts
in view didload:
let nib = UINib(nibName: "YOURCELLNIB", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "REUSEIDENTIFIER")
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
Remove the function
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath)
In your code, you have:
tableHeight = maxHeight!
//set the tableview height to be the content height
But this does not change the table height - it only changes some variable that previously was assigned the value of the old table content height. Nowhere in your code do you actually do anything to change the table height.
One way to change the table height directly is to assign it a completely new frame with values from the old frame, except for the frame's height, which you calculate however you like.
Try something like this (adding whatever other logic you need):
oldFrame = self.tableView.frame
newHeight = rowCount * rowHeight
self.tableView.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.size.width, newHeight)
There is a workaround which can make it seems like the height changes according to the number of the cells.
set tableview height to a proper value when init.
UITableView.init(frame: CGRect.init(x: 0, y: 70, width: self.view.frame.width, height: self.view.frame.height - 350))
set the tableview background color white transparent.
pulldownTableView?.backgroundColor = UIColor.white.withAlphaComponent(0)
set tableFooterView.
pulldownTableView?.tableFooterView = UIView(frame: CGRect.zero)
Below is the result, there are two table in the img. I set the transparent for the front tableview, left img set the backgroundColor to white, right white transparent.
----------------------vs----------------

Dynamic UILabel size for view before uitableview

I am having abit of trouble here trying to make this post description label to grow and shrink based on content size on IOS9. I have a view (I will refer it topView) that I am using as a header for the tableview (So when I scroll up the header disappears). Inside the topView, there are a bunch of stack views. I wish to grow and shrink the post description label in height based on content size. I do know how to do it in simple case where everything is inside the prototype cell (i.e. set estimated row height and set uitableviewautomaticDimensions, set sizetoFit on label and change number of lines to 0). However, this is a different case because the post description label is not really inside the cell, it is in its view before the table view cells.
Note that all items in the view has static height except the postdescription label. Post description label is inside a stack view that is pinned only left and right (So that top and bottom would grow?). Also, the main stack view that contains all elements is pinned towards the four sides with the topview that contains the main stack view also pinned towards the four sides. With this setup, I would expect the topview to grow and shrink based on the content size. However, I do not see that in the output. I dont know if it is the stackview that is holding the label refusing to grow or the top view refusing to grow to allow more space for the stackview for the label. Thanks
UPDATE
Thanks Riadluke, I tried doing something as suggested which is resizing the headerview after calculating the required height. I have placed the following code in viewDidLayoutSubview and it works with an issue
postDescriptionLbl.sizeToFit()
let headerView = commentTableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = TopStackView.frame.size.height + ImageStackView.frame.size.height + postDescriptionLbl.frame.size.height + SpacerStackView.frame.size.height + BottomStackView.frame.size.height
headerView.frame.size.height = height
commentTableView.tableHeaderView = headerView
The issue I now have with this method is that when the view controller appears, I can physically see the postDescription label height grow from the default height in storyboard to the required height. For example, when the VC first appears, I see a line of label with some string being cut off, however after 0.5 second, the headerview and the label grow to the size that I wanted. I know this would be expected because I was calling the manipulation after the viewDidLayout Subview. I was wondering if there is a better way such that I dont see that transition and the view appears to be the right height straight away. Ie. let the view know exactly how high the label is to determine how high the headerview needs to be before appearing on screen?
I'm afraid the view set as tableHeaderView of a UITableView does not get resized automatically. Its height will be fixed to the height it had in IB.
What you have to do is set its size manually and then reassign it as the tableHeaderView so it is displayed in the height you want.
It could take only few lines since you're using autolayout.
You can try this code right after you've set the header view's contents:
//for the target size you have set the width as your tableView's width when it is already displayed on screen.
//note that when it is accesed inside viewDidLoad the tableView's bounds
//may be different to the actual bounds it will be displayed with,
//here I am just using the screen bounds
let targetSize = CGSize(width: UIScreen.mainScreen().bounds.width, height: 10000)
//set the tableHeader's size to its size after its layout constraints are resolved
tableHeader.bounds.size = tableHeader.systemLayoutSizeFittingSize(targetSize)
//reassign it as the tableHeaderView to update the height it will be displayed in
tableView.tableHeaderView = tableHeader
After many many attempts, I could not get anything to work with the original setup. The best I achieved was to resize it after view did appear which is not idea as you see the previous layout.
It is now working with a complete different approach. I have created two prototype cell and have one as "HeaderViewCell" and implemented the following functions
commentTableView.estimatedSectionHeaderHeight = 400
commentTableView.sectionHeaderHeight = UITableViewAutomaticDimension
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
Everything works like a charm after that.

Set UILabel height programmatically Swift not working

I m working on an app where I calculate the height of tableview cell(custom cell) dynamically.The height is calculated perfectly but the label in the cell is truncated. I also tried to set the label's height but still it shows truncated text.
In above screenshot you can see that the long text is not completely shown,
I tried setting the label's height programmatically but it does not work.
Below is the code for setting the height:
let attributes = NSMutableDictionary()
attributes.setValue(MyFonts.HELVETICA_NEUE_REGULAR_15, forKey: NSFontAttributeName)
var cellSize = labelText!.boundingRectWithSize(labelSize!, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: attributes, context: nil)
labelHeight = cellSize.size.height
customCell?.subtitleLabel?.frame.size.height = labelHeight;
Kindly suggest any solution for this.
If your cell is created with auto layout you need to set
customCell?.subtitleLabel?.setTranslatesAutoresizingMaskIntoConstraints(true)
Here you have to take care of some points.
Need to override layoutSubviews() in custom cell, so that you can set frame for that label text
Number of line for label should be zero
Dynamic calculate the height of label
Dynamic cell height
I have created demo for dynamic cell.
Sample demo

Resources