Dynamic height UITableViewCell - ios

I have cells that consist of a title, date and a number of hashtags.
Here's the storyboard's screenshot:
Custom cell in storyboard
I've set the following in my ViewDidLoad:
tableView.rowHeight = UITableViewAutomaticDimension
But haven't given any estimatedHeight for the tableView which I'll explain why.
Here's my customCell:
#IBOutlet weak var title: UILabel!
#IBOutlet weak var date: UILabel!
#IBOutlet weak var hashtagsView: UIView!
var item: Item? {
didSet {
configureCell()
}
}
func configureCell() {
if item = item {
title.text = item!.title
date.text = item!.date
// The part where I calculate the sizes of hashtags to fit them in hashtagsView
let totalWidth = CGRectGetWidth(hashtagsView.frame)
print("TotalWidth: \(totalWidth)")
.
.
.
.
print("Content: \(self.contentView.frame)")
print("HashtagsView: \(self.hashtagsView.frame)")
}
}
Here's the results:
With tableView.estimatedHeight = 150
TotalWidth: 240.0
Content: (0.0, 0.0, 240.0, 119.666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Without estimatedHeight
TotalWidth: 240.0
Content: (0.0, 0.0, 414.0, 43.6666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Recap
When there is an estimatedHeight, cell's contentView doesn't print a correct width, but displays the cell's contents well like nothing's wrong (except for the hashtagsView).
When there's not an estimatedHeight, cell's contentView does actually print the correct width, which lets me calculate the hashtags frames, but the cells display with their default 44 height.
Detailed Info
Since I don't know how many hashtags there is, I'm trying to use the blank UIView to add the UIButtons programmatically. And for calculation purposes of the hashtags' buttons, I need to have the "width" of the cell's contentView, or the hashtagsView's, but when I set tableView.estimatedHeight, cell's width will be some arbitrary number (e.g. 240 in 6s Plus Simulator). And I just can't get the hashtagsView's width, even though I have no auto-layout issues.
And when I don't give tableView.estimatedHeight an estimation, I get the following:
Custom cells without estimatedHeight
Updated - An update asked by #EarlGray in the comments
The hashtags are actually UIButtons I add them to the hashtagsView dynamically. I need to stretch the hashtagsView's height so it'll fit more than one line of hashtags.
I think I'll either need to subclass UIView and override layoutSubviews() to achieve fit vertical layout or add constraints to each subview (UIButtons) programmatically.
Doing what #VinodVishwanath said, setting estimatedRowHeight combined with explicit heights and vertical space constraints gives me this:
Which unables me to get the width of the hashtagsView. During the calculations of the UIBUttons' frames, I need the cell.contentView's width, but somehow, setting the estimatedRowHeight gives me the following coordinates for the cell.contentView.frame
Content: (0.0, 0.0, 240.0, 119.666666666667)
Which is incorrect, because it has to give 414, and that's why my hashtags start from half of the screen.
Commenting out tableView.estimatedRowHeight gives me the correct coordinates:
Content: (0.0, 0.0, 414.0, 43.6666666666667)
But messes my tableView like so:
Update #2 - Here's my constraints for the cell.contentView
ContentView's constraints
Update #3 - A breakpoint on my configureCell method
HashtagsView's superview returns nil!!
I don't get it, my UIView IBOutlet is connected, I double checked.
All of the contentView.subviews have incorrect frames. So does the superview-less hashtagsView.
But when I remove estimatedRowHeight, it suddenly considers hashtagsView as a subview of cell's contentView. Except for contentView, it's subviews frames' still return incorrect and negative values.

When you don't provide the estimated row-height, the cell height defaults to 44, and that's why you're getting a content height of 43.667.
You need to provide the estimated row height, which will be the height when there are no tags in the field provided. Then all that's needed to set the correct height dynamically is the right set of autolayout constraints to provide the contentView's intrinsic size.
This will happen when you set a vertical space constraint between all the subviews of contentView, including a topSpaceToSuperview and bottomSpaceToSuperview to enable the AutoLayout engine to calculate the intrinsic content size.
Edit
I have analysed your constraints from the screenshot, and here are the relevant constraints you have added to calculate the cell height:
Title.top = topMargin + 8
bottomMargin = Share Button.bottom
Link.centerY = Share Button.centerY
Hashtags View.top = Title.bottom
Date.top = Hashtags View.bottom + 15
Now here's a visual representation of these constraints:
TopMargin
|
Title
|
Hashtags View
|
Date
!!! broken link !!!
Link — Share Button
|
BottomMargin
The broken link prevents the Layout Engine from calculating the cell height. You need to fix the broken link in the vertical layout, for instance, by setting Date.centerY = shareButton.centerY, or by setting Date.top = HashtagsView.bottom.

Related

How to corrently use UIViews systemLayoutSizeFitting to get the height to show all subviews using a given width?

TD;DR
It seems that in some cases systemLayoutSizeFitting does not return the correct height to correctly show / position all subviews of a view. Am I using systemLayoutSizeFitting wrong or is there some other way to avoid this?
Long story:
The XIB file of a UIViewController does not only contain the main view but also a number of other views which are added to the view controllers view at runtime. All these additional views should get the same height when they are added to the view controllers view.
The views might look like this: A simple container view holding some subviews which are stacked on top of each other.
Since the height of the container view should be flexible, the vertical spacing between the bottom button and the lable above it, uses a grater-than constraint.
To give all views the same height, I tried to measure the necessary height of each view using systemLayoutSizeFitting:
#IBOutlet var pageViews: [UIView]!
override func viewDidLoad() {
super.viewDidLoad()
var maxHeight: CGFloat = 0
for pageView in pageViews {
// Add pageView somewhere on view and give it leading, trailing and top
// constraint, but no height constraint yet.
addToView(pageView)
maxHeight = max(maxHeight, pageView.systemLayoutSizeFitting(CGSize(width: view.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height)
}
for pageView in pageViews {
// Give all pageViews the same height
pageView.heightAnchor.constraint(equalToConstant: maxHeight).isActive = true
}
}
This does not work, when the label text becomes to long:
In the right example the height is not large enough and thus the button is squeezed. I can counter act this by raising the vertical compression resistance of the button, however in this case the other controls (e.g. the title label) is squeezed...
Why is this? Why does not systemLayoutSizeFitting return a height which is sufficent to show all controls without any squeezing?
Its actually smash button's height when label text is getting bigger . You are setting top and bottom constraints but button height is not declared so when label getting bigger , view basically say "I can reduce button height before updating my height , I have space.Bottom and top constraints are still same , didn't effect."
Giving the constant height constraints of button might be fix your issue.
If you want your view to resist to compression you should use the defaultHigh priority as a verticalFittingPriority instead of fittingSizeLevel.

How to get resizing tableView cell with different content without deleting constraints in iOS

I have tableView cell with different content(views, labels, imageViews) in one cell. But in something cells content can be not full. How can i use resizing cells without removing and adding always constraints? Thanks.
One of possible solutions for this problem:
Add constraints for hidden state with priority 1000
Add extra constraints for resized state with lower priority (ex 750)
Save constraints that is ONLY for hidden state into IBOutlet collection
Save constraints that is ONLY for resized state into another IBOutlet collection
Code:
#IBOutlet var hiddenConstraints: [NSLayoutConstraint] = []
#IBOutlet var visibleConstraints: [NSLayoutConstraint] = []
func hide(_ hide: Bool) {
for hiddenConstraint in self.hiddenConstraints {
hiddenConstraint.isActive = hide
}
for visibleConstraint in self.visibleConstraints {
visibleConstraint.isActive = !hide
}
self.layoutIfNeeded()
}
There is faster solution:
Move content that can be hidden into container view
Set height constraint for container view
Change from code height constraint constant to 0 if hidden or to proper height if visible
Code:
#IBOutlet var heightConstraint: NSLayoutConstraint!
func hide(_ hide: Bool) {
self. heightConstraint.constant = hide ? 0 : 150 //Estimated height
self.layoutIfNeeded()
}
This is not a good approach, as it will lead to constraint crashes at runtime. So I prefer to use first one.
Also you will need to update your cell from table to move other cells up or down.
Ray Wenderlich has a fantastic tutorial on dynamic sizing of table cells that can be found here:
https://www.raywenderlich.com/129059/self-sizing-table-view-cells
TL;DR You need to make sure your cell's content is pinned on all four sides to the cell's content view, as well as setting as high priority vertical hugging, greater than or equal to height constraint on your label.

Auto height UICollectionView

I am using UICollectionView and in it my cells have auto width based on the content(text size) e.g. first row might contain 8 items and 2nd row might contains only 1. This is working fine.
I want to set the height of my UICollectionView to show all the available items but not more(not the empty space at the bottom...). If I use auto layout than I have to set the height or bottom align constraint.
But in this way the height will be fixed. Is there any way I can get number of rows in and calculate height dynamically?
Here is what my story board look like:
You could use fixed height constraint at design time and than change it at runtime when deferred height value calculated. Height constraint need to be referenced in code and than needs to be changed with your calculated height.
// Height constraint of collection view
#IBOutlet weak var heightContstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
setHeightOfCollectionView()
}
func setHeightOfCollectionView() {
// Calculate height of collection view depending on collection item count
let calculatedHeight:CGFloat = 500.0
heightContstraint.constant = calculatedHeight
}
You should check the images below that demonstrate how to do.
Hope it would help.

How to let TextView be able to scroll horizontally

I know the TextView is embedded in a ScrollView. And if there is a fairly long String(Which contains no "\n")
The TextView will automatically do the line-wrap, according to the width of the TextView.
If TextView's height is short, then we are able to scroll it vertically.
How do you disable the auto line-wrap? Such that, if there are no "\n" encounters, it does not line wrap. Rather, it lets the user scroll horizontally to view the text.
How can I implement this?
I figure out how to do this with many helps of you guys :D, thanks, and here we go!
1. So, firstly, We need a longlonglong String, right?
let displayStr = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 "
2. Assume we have a UIScrollView which is linked by #IBOutlet or got by calling the .viewWithTag(xxx) .We will let it be named as scroll :
3. It's time to get the size of our string, here is a key function we'll use it:
Oh! I almost forget to define what kind of Font( this's a crucial parameter ) we will use, and What is the max-size of our string's size
let maxSize = CGSizeMake(9999, 9999)
let font = UIFont(name: "Menlo", size: 16)!
//key function is coming!!!
let strSize = (displayStr as NSString).boundingRectWithSize(maxSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : font], context: nil)
4. OK, now we can put the string into a textView, you need to programmatically create a new UITextView, so that we can define the frame which is identical to the string's size(Oh, maybe a little bigger :D) :
let frame = CGRectMake(0, 0, size.width+50, size.height+10)
let textView = UITextView(frame: frame)
textView.editable = false
textView.scrollEnabled = false//let textView becomes unScrollable
textView.font = font
textView.text = displayStr
5. Back to our scroll, we will set its contentSize:
scroll.contentSize = CGSizeMake(size.width, size.height)
6. Finally, addSubview:
scroll.addSubview(textView)
You can see, textView is embed in a scrollView, which allow it to
scroll with 2 directions.
B.T.W. My implement is just a demo for
static String. if you want user to use a textView which will not line
wrap if he doesn't input any "\n", you may need dynamically calculate
the string size. :D
[I hope this will help]
[myTextView setContentSize:CGSizeMake(width, myTextView.frame.size.height)];
The width of the content extends past the width of the textView's frame or else it won't scroll.
Turn off all the scroll options on the UITextView, then embed it in another UIScrollView. Reference:- DualScrollTextView
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var textView: UITextView!
var yourText: String = "Whatever your text is going to be."
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.layoutIfNeeded()
self.scrollView.contentSize = self.textView.bounds.size
}
override func viewDidLoad() {
super.viewDidLoad()
textView.text = yourText
textView.sizeThatFits(CGSizeMake(textView.frame.size.width, 80)) //height to equal the actual size of your text view.
}
I think this should plug and play, but I know for sure that textView.sizeThatFits works. You have to make sure that you constrain your text view to it's parent scrollview for it to work. (Leading, Trailing, Top, Bottom and the Height.)
The accepted answer didn't work with AutoLayout so I'll share my approach:
1. Add a UIScrollView with a UITextView inside it and pin all the edges for both of them
2. Add width and height constraints for your UITextView (doesn't matter what you set them to)
3. Create IBOutlets for the UITextView, the height constraint, and the width constraint
4. Uncheck 'Scrolling Enabled' on the TextView
5. When you update the text, calculate the bounding size of the text and update the height and width constraints with the bounding size
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var textViewWidthConstraint: NSLayoutConstraint!
let displayText = "Your text here."
override func viewDidLoad() {
super.viewDidLoad()
let maxSize = CGSize(width: 10000, height: 30000)
let textRect = displayText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font : self.textView.font!], context: nil)
self.textView.text = displayText
self.textViewHeightConstraint.constant = textRect.height
self.textViewWidthConstraint.constant = textRect.width
}
UPDATE: I discovered that this approach uses a lot of memory for large amounts of text (I was seeing over 1GB used). Here's how I reduced the memory impact from 1GB to 100MB:
1. Set the edge constraints just like step 1 above.
2. Add a width constraint to the TextView for the scrollable content width (I used 2000 but you can adjust it to your liking, the wider you go the more memory you'll use though).
3. Add a constraint to make the TextView and ScrollView have equal heights
That's it! The scrollable width will be constant and once you set the text for the TextView the scrollable height will automatically adjust.
Note: Some caveats of this approach:
The scrollable width will always be the width you set but you can use a hybrid approach with the first solution if you want to make it sized based on the text
You can't see the vertical scroll bar (unless you're scrolled all the way to the right) but it's possible to get it back by adjusting the scroll bar inset of the TextView.
Reference: https://www.ralfebert.de/ios-examples/auto-layout/uiscrollview-storyboard/

Get View height based on margin constraints after runtime Swift

I have a circle in the centre of a screen with a margin constraint of 50 on either end. Hence, the width of the circle is dependent on the screen size.
So, what I tried was this:
Approach 1
I set up the margins in the storyboard to define the circle width (50 on left and right)
Then I used the following code:
#IBOutlet weak var helpButHeight: NSLayoutConstraint!
#IBOutlet weak var helpBut: UIButton!
ViewDidLoad:
helpButHeight.constant = helpBut.frame.size.width
This didn't work.
Since the screen width is 400, and the margin is 50 on either end, then helpBut.frame.size.width should have given me 300.
Instead it gave me 46.
Approach 2
This was my work-around:
ViewDidLoad:
let screenSize: CGRect = UIScreen.mainScreen().bounds
helpButHeight.constant = screenSize.width - 100
because 100 = 50 + 50, the two margins.
Works fine !
Question
Why did I have to do this? Why did the first approach not work? Why 46 and not 300?
The reason is that constraints haven't kicked in, in the viewDidLoad function. The lifecycle looks something like
viewDidLoad -- Constraints haven't set
viewWillAppear -- Constraints haven't set
viewWillLayoutSubviews -- Constraints are setting
viewDidLayoutSubviews -- Constraints are set
viewDidAppear -- Constraints are set
If you want any view to be in center just put the horizontal/vertical center constraint...No code required.. If you want to have padding just put the left and right constraints...Just to remind you don't use both of them together...It'll break...

Resources