How do I properly resize a UICollectionView so that it fully displays its contents? I have tried many things, including setting its frame, calling reloadData and invalidating the layout:
self.collectionView.contentSize = CGSizeMake(300, 2000);
self.collectionView.frame = CGRectMake(0, 0, 300, 2000);
[self.collectionView reloadData];
[self.collectionView.collectionViewLayout invalidateLayout];
but none of this has any effect. After pressing the button I still see the initial view, like this:
I have a small demo program where I have a data source producing 100 elements. In Interface Builder I initially set the size of the UICollectionView to a small value so that not all elements fit, after that I press a button after which the code above is executed. I expect the UICollectionView to now show all elements, but it doesn't.
EDIT: The demo program can be found at https://github.com/mjdemilliano/TestUICollectionView.
EDIT2: I have observed that the frame update is lost at some point, because if I press the button again, the current frame is back to the old value. After adding some log statements in the button event handler, the log output is:
before: frame = {{0, 58}, {320, 331}}, contentSize = {320, 1190}
update button pressed
after: frame = {{0, 0}, {300, 2000}}, contentSize = {300, 2000}
before: frame = {{0, 58}, {320, 331}}, contentSize = {320, 1190}
update button pressed
after: frame = {{0, 0}, {300, 2000}}, contentSize = {300, 2000}
I don't understand why the frame change is not kept, what is changing it.
At some point I will replace the hardcoded values by values obtained from the flow layout, but I wanted to rule that out and keep my example as simple as possible.
Context: What I want to do eventually is the following: I have a scrollable view with various controls like labels and images, and a collection view with dynamic content. I want to scroll all that, not just the collection view, therefore I am not using the collection view's own scrolling facilities, which work fine.
I solved this eventually by fixing all Auto Layout issues, fixing the height of the collection view using a constraint. Then, whenever I know the content has changed I update the value of the constraint using the value collectionView.contentSize.height:
self.verticalLayoutConstraint.constant = self.collectionView.collectionViewLayout.collectionViewContentSize.height;
Then the collection view is resized properly and it behaves nicely within the overall scrollview. I have updated the GitHub test project with my changes.
To me, doing this by updating the constraint manually instead of being able to tell iOS: "make the frame height of the collection view as large as needed" does not feel right to me, but it's the best I have come up with so far. Please post a better answer if you have one.
It seems to work nicely with a custom UICollectionView class.
class AutoSizedCollectionView: UICollectionView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
Set your custom class in the interface builder:
This way you can also set your collection views intrinsic size to 'placeholder' in interface builder to avoid having to set a height constraint.
I hope this helps someone else.
Here's my implementation in Swift 3:
override func sizeThatFits(_ size: CGSize) -> CGSize {
if (self.superview != nil) {
self.superview?.layoutIfNeeded()
}
return collectionView.contentSize
}
UICollectionViewFlowLayout *flowLayout;
flowLayout = [[UICollectionViewFlowLayout alloc]init];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
[flowLayout setMinimumInteritemSpacing:0.0f];
[flowLayout setMinimumLineSpacing:0.0f];
[self.collectionView setPagingEnabled:NO];
[flowLayout setItemSize:CGSizeMake(322.0, 148.0)]; //important to leave no white space between the images
[self.collectionView setCollectionViewLayout:flowLayout];
I found that autolayout in the storyboard is not helping too much. A correct setting for the UICollectionViewFlowLayout for your collectionView is the real help. If you adjust item size with setItemSize, you may get the result you want.
The simplest method I found is to override sizeThatFits: methods as is:
- (CGSize)sizeThatFits:(CGSize)size
{
if( self.superview )
[self.superview layoutIfNeeded]; // to force evaluate the real layout
return self.collectionViewLayout.collectionViewContentSize;
}
Here's a way to bind the CollectionView's height via it's intrinsic size.
I used it to properly size a CollectionView inside a TableView Cell (with dynamic cells height). and it works perfectly.
First, add this to your UICollectionView subclass:
override var intrinsicContentSize: CGSize {
get {
return self.contentSize
}
}
Then call layoutIfNeeded() after you reload data:
reloadData()
layoutIfNeeded()
You can try out my custom AGCollectionView class
Assign a height constraint of collectionView using a storyboard or programmatically.
- Assign this class to your UICollectionView.
class AGCollectionView: UICollectionView {
fileprivate var heightConstraint: NSLayoutConstraint!
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.associateConstraints()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.associateConstraints()
}
override open func layoutSubviews() {
super.layoutSubviews()
if self.heightConstraint != nil {
self.heightConstraint.constant = floor(self.contentSize.height)
}
else{
self.sizeToFit()
print("Set a heightConstraint set size to fit content")
}
}
func associateConstraints() {
// iterate through height constraints and identify
for constraint: NSLayoutConstraint in constraints {
if constraint.firstAttribute == .height {
if constraint.relation == .equal {
heightConstraint = constraint
}
}
}
}
}
Add IBOutlet for CollectionView Height Constraint
--> Like #IBOutlet weak var collectionViewHeight: NSLayoutConstraint!
Add Below snipped code.
For me it is even simpler I think
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//add following code line after adding cells, before Return
...........
.........
scrollView.contentSize = = collectionView.contentSize;
//now scrollView size is equal to collectionView size. No matter how small or big it is.
return cell;
}
Related
I'm trying to build something
I'm trying to build a tag list view using UICollectionView and nest it into my custom UITableViewCell.
What do I have now
After searching the internet, I find the key to the problem:
Subclass UICollectionView and implement it's intrinsic content size property.
However, when I nest my custom UICollectionView into a self-sizing UITableViewCell, the whole thing doesn't work well. The layout is broken.
No matter how do I change the code, I get one of the following 3 buggy UIs.
The height of the collection view is always wrong, either too small or too large, it can not hug it's content just right.
When I use Debug View Hierarchy to check the views, I find that although the UI is broken, the contentSize property of the collection view has a correct value. It seems that the content size property can not be reflected to the UI in time.
class IntrinsicCollectionView: UICollectionView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
isScrollEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
There are many solution about how to create a custom UICollectionView with intrinsic content size. Some of them can work correctly. But when nesting them into a UITableViewCell, none of them works well.
There are also some answer about just nest one UICollectionView into UITableViewCell without other views. But if there are also some UILabel in UITableViewCell, it won't work.
I upload all the code to github. https://github.com/yunhao/nest-collectionview-in-tableviewcell
Thank you!
I'll try to explain what's going on....
To make it easy to understand, in your ListViewController let's work with just one row to begin with:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1 // items.count
}
In your ListViewCell class, add these lines at the end of prepareViews():
// so we can see the element frames
titleLabel.backgroundColor = .green
subtitleLabel.backgroundColor = .cyan
collectionView.backgroundColor = .yellow
In your IntrinsicCollectionView class, let's add a print() statement to give us some information:
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
// add this line
print("collView Width:", frame.width, "intrinsic height:", collectionViewLayout.collectionViewContentSize.height)
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
When I then run the app on an iPhone 8, I get this result:
and I see this in the debug console:
collView Width: 66.0 intrinsic height: 350.0
collView Width: 343.0 intrinsic height: 30.0
What that tells me is that the collection view is asked for its intrinsicContentSize before it has a complete frame.
At that point, it fills in its cells, and its layout ends up with a .collectionViewContentSize.height of 350 (this row has six "tags" cells).
Auto-layout then performs another pass... the collection view now has a valid frame width (based on the cell width)... and the cells are re-laid-out.
Unfortunately, the table view has already set the row height(s), based on the initial collection view intrinsicContentSize.height.
So, two steps that may (should) fix this:
In ListViewCell, invalidate the content size of the collection view when you get the tags:
func setTags(_ tags: [String]) {
self.tags = tags
collectionView.reloadData()
// add this line
collectionView.invalidateIntrinsicContentSize()
}
Then, in ListViewController, we need to reload the table after its frame has changed:
// add this var
var currentWidth: CGFloat = 0
// implement viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if view.frame.width != currentWidth {
currentWidth = view.frame.width
tableView.reloadData()
}
}
That seems (with very quick testing) to give me reliable results:
and on device rotation:
I would like the UICollectionView (The red one) to shrink to the height of the content size in this case UICollectionViewCells(the yellow ones) because there is a lot of empty space. What I tried is to use:
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return self.collection.contentSize
}
but return self.collection.contentSize always return (width, 0)
and for this reason it shrinks too much to value of height 30 (The value which I set in the XIB file for the height, although I have constaint >= 30).
I would suggest the following:
Add a height constraint to your collection view.
Set its priority to 999.
Set its constant to any value that makes it reasonably visible on the storyboard.
Change the bottom equal constraint of the collection view to greater or equal.
Connect the height constraint to an outlet.
Every time you reload the data on the collection view do the following:
You may also want to consider the Inset of the collection view by adding it to the content size.
Code Sample:
CGFloat height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
heightConstraint.constant = height
self.view.setNeedsLayout() Or self.view.layoutIfNeeded()
Explanation: Extra, You don't have to read if you understand it. obviously!!
The UI will try to reflect all the constraints no matter what are their priorities. Since there is a height constraint with lower priority of (999), and a bottom constraint of type greater or equal. whenever, the height constraint constant is set to a value less than the parent view height the collection view will be equal to the given height, achieving both constraints.
But, when the height constraint constant set to a value more than the parent view height both constraints can't be achieved. Therefore, only the constraint with the higher priority will be achieved which is the greater or equal bottom constraint.
The following is just a guess from an experience. So, it achieves one constrant. But, it also tries to make the error in the resulted UI for the other un-achieved lower priority constraint as lowest as possible. Therefore, the collection view height will be equal to the parent view size.
In Swift 5 and Xcode 10.2.1
My CollectionView name is myCollectionView
Fix height for your CollectionView
Create Outlet for your CollectionViewHeight
IBOutlet weak var myCollectionViewHeight: NSLayoutConstraint!
Use below code
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = myCollectionView.collectionViewLayout.collectionViewContentSize.height
myCollectionViewHeight.constant = height
self.view.layoutIfNeeded()
}
Dynamic width for cell based on text content...
Dynamic cell width of UICollectionView depending on label width
1) Set Fix Height of your CollectionView.
2) Create Outlet of this CollectionView Height Constant.
Like :
IBOutlet NSLayoutConstraint *constHeight;
3) Add below method in your .m file:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat height = collectionMenu.collectionViewLayout.collectionViewContentSize.height;
constHeight.constant = height;
}
I ended up, by subclassing the UICollectionView and overriding some methods as follows.
Returning self.collectionViewLayout.collectionViewContentSize for intrinsicContentSize makes sure, to always have the correct size
Then just call it whenever it might change (like on reloadData)
Code:
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return self.collectionViewLayout.collectionViewContentSize
}
But be aware, that you lose "cell re-using", if you display large sets of data, eventhough they don't fit on the screen.
This seemed like the simplest solution for me.
class SelfSizingCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
isScrollEnabled = false
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return contentSize
}
}
You may not need to override reloadData
You have to set height constraint as equal to content size
HeightConstraint.constant = collection.contentSize.height
Took the solution by d4Rk which is great, except in my case it would keep cutting off the bottom of my collection view (too short). I figured out this was because intrinsic content size was sometimes 0 and this would throw off the calculations. IDK. All I know is this fixed it.
import UIKit
class SelfSizedCollectionView: UICollectionView {
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
let s = self.collectionViewLayout.collectionViewContentSize
return CGSize(width: max(s.width, 1), height: max(s.height,1))
}
}
Subclass UICollectionView as follows
Delete height constraint if any
Turn on Intrinsic Size
-
class ContentSizedCollectionView: UICollectionView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}
}
If you set the height constraint of the collection view. Just observe the contentSize change in the viewDidLoad and update the constraint.
self.contentSizeObservation = collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, change in
guard let `self` = self else { return }
guard self.collectionView.contentSize != .zero else { return }
self.collectionViewHeightLayoutConstraint.constant = self.collectionView.contentSize.height
}
I have a multi-line, multi-selection UICollectionView subclass where the cells are of fixed height and left-aligned flowing from left to right. It's embedded in a vertical stack view that's inside a vertical scroll view. See the UI component below the label "Property Types".
In order for the collection view to fit the height of its contentSize, here's what I had to do (note that this is all within the UICollectionView subclass):
Give the collection view a non-zero minimum height constraint of priority 999. Auto-sizing the collection view to its content height simply won't work with zero height.
let minimumHeight = heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
minimumHeight.priority = UILayoutPriority(999)
minimumHeight.isActive = true
Set the collection view's content hugging priority to .required for the vertical axis.
setContentHuggingPriority(.required, for: .vertical)
Calling reloadData() is followed by the following calls:
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
For example, I have a setItems() function in my subclass:
func setItems(_ items: [Item]) {
self.items = items
selectedIndices = []
reloadData()
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
}
Override contentSize and intrinsicContentSize as follows:
override var intrinsicContentSize: CGSize {
return contentSize
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
layoutIfNeeded()
}
}
Do following.
first set height constrain for UICollectionView
here calendarBaseViewHeight is UICollectionView height Variable
call the function after reload the collection view
func resizeCollectionViewSize(){
calendarBaseViewHeight.constant = collectionView.contentSize.height
}
first of all calculate number of cells than multiply it with height of cell and then return height in this method
collectionView.frame = CGRectMake (x,y,w,collectionView.collectionViewLayout.collectionViewContentSize.height); //objective c
//[collectionView reloadData];
collectionView.frame = CGRect(x: 0, y: 0, width: width, height: collectionView.collectionViewLayout.collectionViewContentSize.height) // swift
On your UICollectionView set your constraints such as Trailing, Leading, and Bottom:
If you look at my height constraint in more detail, as it is purely for storyboard look so I don't get errors, I have it to Remove at build time. The real height constraint is set in my code down below.
My code for DrawerCollectionView, which is set as the collection view Custom Class:
import UIKit
class DrawerCollectionView: UICollectionView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
heightAnchor.constraint(equalToConstant: contentSize.height).isActive = true
}
}
Adjusting height of UICollectionView to the height of it's content size 🙌🏻
SWIFT 5
final class MyViewController: UIViewController {
// it's important to declare layout as separate constant due to late update in viewDidLayoutSubviews()
private let layout = UICollectionViewFlowLayout()
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupCollectionViewConstraints()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateFlowLayout()
}
private func setupCollectionView() {
view.addSubview(collectionView)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
collectionView.dataSource = self
}
private func setupCollectionViewConstraints() {
// your collectionView constraints setup
}
private func updateFlowLayout() {
let height = collectionView.collectionViewLayout.collectionViewContentSize.height
layout.itemSize = CGSize(width: view.frame.width, height: height)
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = .zero
layout.minimumLineSpacing = .zero
layout.sectionInset = UIEdgeInsets.zero
}
}
extension MyViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {...}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {...}
}
work for me
let heightRes = resCollectionView.collectionViewLayout.collectionViewContentSize.height
foodHeightConstrant.constant = height.advanced(by: 1 )
foodCollectionView.setNeedsLayout()
foodCollectionView.layoutIfNeeded()
I was using a UICollectionView in UITableView cell. For me, the following solution worked.
In parent view of collection view, I updated the height constraint in layoutSubviews method like this
override func layoutSubviews() {
heightConstraint.constant = myCollectionView.collectionViewLayout.collectionViewContentSize.height
}
and then in cellForRowAtIndexpath, just before returning the cell, call this
cell.layoutIfNeeded()
The only solution worked for me when CollectionView is inside TableView custom cell is to
Subclass from ContentSizedCollectionView:
final class ContentSizedCollectionView: UICollectionView {
override var contentSize: CGSize{
didSet {
if oldValue.height != self.contentSize.height {
invalidateIntrinsicContentSize()
}
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric,
height: contentSize.height)
}
}
private let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 20, left: 17, bottom: 20, right: 17)
let collectionView = ContentSizedCollectionView(frame: .zero, collectionViewLayout: layout).prepareForAutoLayout()
return collectionView
}()
In UITableViewDelegate for TableView cell:
if let reusableCell = cell as? YourTableViewCell {
reusableCell.frame = tableView.bounds
reusableCell.layoutIfNeeded()
}
Remove height constraints of UICollectionView if any.
This article helped me a lot: https://medium.com/#ar.sarris/self-sizing-collectionview-inside-a-tableview-f1fd4f42c44d
Get the height of the cell. Something like this
let cellHeight = cell.frame.height
Get the origin of the collection view
let cvOrigin = collectionView.frame.origin
Get the width of the collection view
let cvWidth = collectionView.bounds.width
Set the frame of the content view
collection.frame = CGRect(x: cvOrigin.x, y: cvOrigin.y, width: cvWidth, height: cellHeight )
TL;DR: When UITableView contentSize is correct?
To update my table height constraint I am using the following function:
func adjustHeightOfTableview()
{
self.myTableView.layoutIfNeeded()
let newHeight = self.myTableView.contentSize.height
self.tableHeightConstraint.constant = newHeight
self.view.setNeedsUpdateConstraints()
}
I tried these 2 solutions:
Calling adjustHeightOfTableview in viewDidLayoutSubviews
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
self.adjustHeightOfTableview()
}
Result: The table did not appear at all on the screen. I guess it is because content size is 0 when viewDidLayoutSubviews called first time. Since table height is set to 0, no cell will be created and viewDidLayoutSubviews is not called again
Calling adjustHeightOfTableview after table reload (I have extention for that)
extension UITableView
{
func reloadData(completion: ()->()) {
UIView.animateWithDuration(0, animations: { self.reloadData() })
{ _ in completion() }
}
}
//in my class:
self.relatedTableView.reloadData()
{
self.adjustHeightOfTableview()
}
Result: table was to heigh. Wrong contentSize??
Bottom line: How and where it is best to update UITableView height NSLayoutConstraint?
Found it! The answer was here: Height adjusted UITableView using Auto Layout
The height constraint needed to be low priority and it worked.
I've updated the constraint both after reloading table and in viewDidLayoutSubviews as described in the question
Long time lurker - first time poster!
I am having an issue while recreating a bar with a UITextView like WhatsApp does it.
I am using a custom UIView subclass, and lazily instantiating it on:
- (UIView *)inputAccessoryView
and returning YES on:
- (BOOL)canBecomeFirstResponder
Now, I want to change the size of the inputAccessoryView when the UITextView grows in size. On iOS 7, I would simply change the size of the frame of said view - and not it's origin -, and then call reloadInputViews and it would work: the view would be moved upwards so that it is fully visible above the keyboard.
On iOS 8, however, this does not work. The only way to make it work is to also change the origin of the frame to a negative value. This would be fine, except it creates some weird bugs: for example, the UIView returns to the 'original' frame when entering any text.
Is there something I am missing? I am pretty certain WhatsApp uses inputAccessoryView because of the way they dismiss the keyboard on drag - only in the latest version of the app.
Please let me know if you can help me out! Or if there is any test you would like me to run!
Thank you! :)
BTW, here is the code I am using to update the height of the custom UIView called composeBar:
// ComposeBar frame size
CGRect frame = self.composeBar.frame;
frame.size.height += heightDifference;
frame.origin.y -= heightDifference;
self.composeBar.frame = frame;
[self.composeBar.textView reloadInputViews]; // Tried with this
[self reloadInputViews]; // and this
Edit: full source code is available # https://github.com/manuelmenzella/SocketChat-iOS
I've been banging my head against the wall on this one for quite some time, as the behavior changed from iOS 7 to iOS 8. I tried everything, until the most obvious solution of all worked for me:
inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
duh!
To sum up JohnnyC's answer: set your inpitAccessoryView's autoresizingMask to .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.
Full code, updated for Swift 3:
class InputAccessoryView: UIView, UITextViewDelegate {
let textView = UITextView()
override var intrinsicContentSize: CGSize {
// Calculate intrinsicContentSize that will fit all the text
let textSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
return CGSize(width: bounds.width, height: textSize.height)
}
override init(frame: CGRect) {
super.init(frame: frame)
// This is required to make the view grow vertically
autoresizingMask = .flexibleHeight
// Setup textView as needed
addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))
textView.delegate = self
// Disabling textView scrolling prevents some undesired effects,
// like incorrect contentOffset when adding new line,
// and makes the textView behave similar to Apple's Messages app
textView.isScrollEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
// Re-calculate intrinsicContentSize when text changes
invalidateIntrinsicContentSize()
}
}
The issue is that in iOS 8, an NSLayoutConstraint that sets the inputAccessoryView's height equal to its initial frame height is installed automatically. In order to fix the layout problem, you need to update that constraint to the desired height and then instruct your inputAccessoryView to lay itself out.
- (void)changeInputAccessoryView:(UIView *)inputAccessoryView toHeight:(CGFloat)height {
for (NSLayoutConstraint *constraint in [inputAccessoryView constraints]) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = height;
[inputAccessoryView layoutIfNeeded];
break;
}
}
}
Here's a complete, self-contained solution (thanks #JohnnyC and #JoĂŁoNunes for pointing me in the right direction, #stigi for explaining how to animate intrinsicContent changes):
class InputAccessoryView: UIView {
// InputAccessoryView is instantiated from nib, but it's not a requirement
override func awakeFromNib() {
super.awakeFromNib()
autoresizingMask = .FlexibleHeight
}
override func intrinsicContentSize() -> CGSize {
let exactHeight = // calculate exact height of your view here
return CGSize(width: UIViewNoIntrinsicMetric, height: exactHeight)
}
func somethingDidHappen() {
// invalidate intrinsic content size, animate superview layout
UIView.animateWithDuration(0.2) {
self.invalidateIntrinsicContentSize()
self.superview?.setNeedsLayout()
self.superview?.layoutIfNeeded()
}
}
}
100% working and very simple solution is to enumerate all constraints and set new height value. Here is some C# code (xamarin):
foreach (var constraint in inputAccessoryView.Constraints)
{
if (constraint.FirstAttribute == NSLayoutAttribute.Height)
{
constraint.Constant = newHeight;
}
}
Unfortunately, iOS8 adds a private height constraint to the inputAccessoryView, and this constraint is not public.
I recommend recreating the accessory view when its frame should change, and call reloadInputViews so that the new one is installed.
This is what I do, and it works as expected.
Yep, iOS8 adds a private height constraint to the inputAccessoryView.
Taking into account that recreating whole inputAccessoryView and replace old one is can be really expensive operation, you can just remove constraints before reload input views
[inputAccessoryView removeConstraints:[inputAccessoryView constraints]];
[textView reloadInputViews];
Just another workaround
To fix this I used inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
But of course this caused my textview to collapse.
So adding a constraint to the toolbar and updating it when I have to, or adding the constraint to the textview itself and update it worked for me.
frist, get inputAccessoryView and set nil
UIView *inputAccessoryView = yourTextView.inputAccessoryView;
yourTextView.inputAccessoryView = nil;
[yourTextView reloadInputViews];
then set frame and layout
inputAccessoryView.frame = XXX
[inputAccessoryView setNeedsLayout];
[inputAccessoryView layoutIfNeeded];
last set new inputAccessoryView again and reload
yourTextView.inputAccessoryView = inputAccessoryView;
[yourTextView reloadInputViews];
I'm trying to get self sizing UICollectionViewCells working with Auto Layout, but I can't seem to get the cells to size themselves to the content. I'm having trouble understanding how the cell's size is updated from the contents of what's inside the cell's contentView.
Here's the setup I've tried:
Custom UICollectionViewCell with a UITextView in its contentView.
Scrolling for the UITextView is disabled.
The contentView's horizontal constraint is: "H:|[_textView(320)]", i.e. the UITextView is pinned to the left of the cell with an explicit width of 320.
The contentView's vertical constraint is: "V:|-0-[_textView]", i.e. the UITextView pinned to the top of the cell.
The UITextView has a height constraint set to a constant which the UITextView reports will fit the text.
Here's what it looks like with the cell background set to red, and the UITextView background set to Blue:
I put the project that I've been playing with on GitHub here.
This answer is outdated from iOS 14 with the addition of compositional layouts. Please consider updating the new API
Updated for Swift 5
preferredLayoutAttributesFittingAttributes renamed to preferredLayoutAttributesFitting and use auto sizing
Updated for Swift 4
systemLayoutSizeFittingSize renamed to systemLayoutSizeFitting
Updated for iOS 9
After seeing my GitHub solution break under iOS 9 I finally got the time to investigate the issue fully. I have now updated the repo to include several examples of different configurations for self sizing cells. My conclusion is that self sizing cells are great in theory but messy in practice. A word of caution when proceeding with self sizing cells.
TL;DR
Check out my GitHub project
Self sizing cells are only supported with flow layout so make sure thats what you are using.
There are two things you need to setup for self sizing cells to work.
#1. Set estimatedItemSize on UICollectionViewFlowLayout
Flow layout will become dynamic in nature once you set the estimatedItemSize property.
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
#2. Add support for sizing on your cell subclass
This comes in 2 flavours; Auto-Layout or custom override of preferredLayoutAttributesFittingAttributes.
Create and configure cells with Auto Layout
I won't go to in to detail about this as there's a brilliant SO post about configuring constraints for a cell. Just be wary that Xcode 6 broke a bunch of stuff with iOS 7 so, if you support iOS 7, you will need to do stuff like ensure the autoresizingMask is set on the cell's contentView and that the contentView's bounds is set as the cell's bounds when the cell is loaded (i.e. awakeFromNib).
Things you do need to be aware of is that your cell needs to be more seriously constrained than a Table View Cell. For instance, if you want your width to be dynamic then your cell needs a height constraint. Likewise, if you want the height to be dynamic then you will need a width constraint to your cell.
Implement preferredLayoutAttributesFittingAttributes in your custom cell
When this function is called your view has already been configured with content (i.e. cellForItem has been called). Assuming your constraints have been appropriately set you could have an implementation like this:
//forces the system to do one layout pass
var isHeightCalculated: Bool = false
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//Exhibit A - We need to cache our calculation to prevent a crash.
if !isHeightCalculated {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
newFrame.size.width = CGFloat(ceilf(Float(size.width)))
layoutAttributes.frame = newFrame
isHeightCalculated = true
}
return layoutAttributes
}
NOTE On iOS 9 the behaviour changed a bit that could cause crashes on your implementation if you are not careful (See more here). When you implement preferredLayoutAttributesFittingAttributes you need to ensure that you only change the frame of your layout attributes once. If you don't do this the layout will call your implementation indefinitely and eventually crash. One solution is to cache the calculated size in your cell and invalidate this anytime you reuse the cell or change its content as I have done with the isHeightCalculated property.
Experience your layout
At this point you should have 'functioning' dynamic cells in your collectionView. I haven't yet found the out-of-the box solution sufficient during my tests so feel free to comment if you have. It still feels like UITableView wins the battle for dynamic sizing IMHO.
##Caveats
Be very mindful that if you are using prototype cells to calculate the estimatedItemSize - this will break if your XIB uses size classes. The reason for this is that when you load your cell from a XIB its size class will be configured with Undefined. This will only be broken on iOS 8 and up since on iOS 7 the size class will be loaded based on the device (iPad = Regular-Any, iPhone = Compact-Any). You can either set the estimatedItemSize without loading the XIB, or you can load the cell from the XIB, add it to the collectionView (this will set the traitCollection), perform the layout, and then remove it from the superview. Alternatively you could also make your cell override the traitCollection getter and return the appropriate traits. It's up to you.
In iOS10 there is new constant called UICollectionViewFlowLayout.automaticSize (formerly UICollectionViewFlowLayoutAutomaticSize), so instead:
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
you can use this:
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
It has better performance especially when cells in your collection view have constant width.
Accessing Flow Layout:
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
Swift 5 Updated:
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
A few key changes to Daniel Galasko's answer fixed all my problems. Unfortunately, I don't have enough reputation to comment directly (yet).
In step 1, when using Auto Layout, simply add a single parent UIView to the cell. EVERYTHING inside the cell must be a subview of the parent. That answered all of my problems. While Xcode adds this for UITableViewCells automatically, it doesn't (but it should) for UICollectionViewCells. According to the docs:
To configure the appearance of your cell, add the views needed to present the data item’s content as subviews to the view in the contentView property. Do not directly add subviews to the cell itself.
Then skip step 3 entirely. It isn't needed.
In iOS 10+ this is a very simple 2 step process.
Ensure that all your cell contents are placed within a single UIView (or inside a descendant of UIView like UIStackView which simplifies autolayout a lot). Just like with dynamically resizing UITableViewCells, the whole view hierarchy needs to have constraints configured, from the outermost container to the innermost view. That includes constraints between the UICollectionViewCell and the immediate childview
Instruct the flowlayout of your UICollectionView to size automatically
yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Add flowLayout on viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
}
}
Also, set an UIView as mainContainer for your cell and add all required views inside it.
Refer to this awesome, mind-blowing tutorial for further reference:
UICollectionView with autosizing cell using autolayout in iOS 9 & 10
EDIT 11/19/19: For iOS 13, just use UICollectionViewCompositionalLayout with estimated heights. Don't waste your time dealing with this broken API.
After struggling with this for some time, I noticed that resizing does not work for UITextViews if you don't disable scrolling:
let textView = UITextView()
textView.scrollEnabled = false
contentView anchor mystery:
In one bizarre case this
contentView.translatesAutoresizingMaskIntoConstraints = false
would not work. Added four explicit anchors to the contentView and it worked.
class AnnoyingCell: UICollectionViewCell {
#IBOutlet var word: UILabel!
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
and as usual
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
in YourLayout: UICollectionViewFlowLayout
Who knows? Might help someone.
Credit
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
stumbled on to the tip there - never saw it anywhere else in all the 1000s articles on this.
I did a dynamic cell height of collection view. Here is git hub repo.
And, dig out why preferredLayoutAttributesFittingAttributes is called more than once. Actually, it will be called at least 3 times.
The console log picture :
1st preferredLayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);
The layoutAttributes.frame.size.height is current status 57.5.
2nd preferredLayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);
The cell frame height changed to 534.5 as our expected. But, the collection view still zero height.
3rd preferredLayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);
You can see the collection view height was changed from 0 to 477.
The behavior is similar to handle scroll:
1. Before self-sizing cell
2. Validated self-sizing cell again after other cells recalculated.
3. Did changed self-sizing cell
At beginning, I thought this method only call once. So I coded as the following:
CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;
This line:
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
will cause system call infinite loop and App crash.
Any size changed, it will validate all cells' preferredLayoutAttributesFittingAttributes again and again until every cells' positions (i.e frames) are no more change.
In addition to above answers,
Just make sure you set estimatedItemSize property of UICollectionViewFlowLayout to some size and do not implement sizeForItem:atIndexPath delegate method.
That's it.
The solution comprises 3 simple steps:
Enabling dynamic cell sizing
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Set the containerView.widthAnchor.constraint from collectionView(:cellForItemAt:)to limit the width of contentView to width of collectionView.
class ViewController: UIViewController, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
cell.textView.text = dummyTextMessages[indexPath.row]
cell.maxWidth = collectionView.frame.width
return cell
}
...
}
class MultiLineCell: UICollectionViewCell{
....
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
}
}
....
}
Since you want to enable self-sizing of UITextView, it has an additional step to;
3. Calculate and set the heightAnchor.constant of UITextView.
So, whenever the width of contentView is set we'll adjust height of UITextView along in didSet of maxWidth.
Inside UICollectionViewCell:
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
let sizeToFitIn = CGSize(width: maxWidth, height: CGFloat(MAXFLOAT))
let newSize = self.textView.sizeThatFits(sizeToFitIn)
self.textViewHeightContraint.constant = newSize.height
}
}
These steps will get you the desired result.
Complete runnable gist
Reference: Vadim Bulavin blog post - Collection View Cells Self-Sizing: Step by Step Tutorial
Screenshot:
If you implement UICollectionViewDelegateFlowLayout method:
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath
When you call collectionview performBatchUpdates:completion:, the size height will use sizeForItemAtIndexPath instead of
preferredLayoutAttributesFittingAttributes.
The rendering process of performBatchUpdates:completion will go through the method preferredLayoutAttributesFittingAttributes but it ignores your changes.
To whomever it may help,
I had that nasty crash if estimatedItemSize was set. Even if I returned 0 in numberOfItemsInSection. Therefore, the cells themselves and their auto-layout were not the cause of the crash... The collectionView just crashed, even when empty, just because estimatedItemSize was set for self-sizing.
In my case I reorganized my project, from a controller containing a collectionView to a collectionViewController, and it worked.
Go figure.
For anyone who tried everything without luck, this is the only thing that got it working for me.
For the multiline labels inside cell, try adding this magic line:
label.preferredMaxLayoutWidth = 200
More info: here
Cheers!
The example method above does not compile. Here is a corrected version (but untested as to whether or not it works.)
override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
{
let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
var newFrame = attr.frame
self.frame = newFrame
self.setNeedsLayout()
self.layoutIfNeeded()
let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
newFrame.size.height = desiredHeight
attr.frame = newFrame
return attr
}
Update more information:
If you use flowLayout.estimatedItemSize, suggest use iOS8.3 later version. Before iOS8.3, it will crash [super layoutAttributesForElementsInRect:rect];.
The error message is
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
Second, in iOS8.x version, flowLayout.estimatedItemSize will cause different section inset setting did not work. i.e. function: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:.
I tried using estimatedItemSize but there were a bunch of bugs when inserting and deleting cells if the estimatedItemSize was not exactly equal to the cell's height. i stopped setting estimatedItemSize and implemented dynamic cell's by using a prototype cell. here's how that's done:
create this protocol:
protocol SizeableCollectionViewCell {
func fittedSize(forConstrainedSize size: CGSize)->CGSize
}
implement this protocol in your custom UICollectionViewCell:
class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {
#IBOutlet private var mTitle: UILabel!
#IBOutlet private var mDescription: UILabel!
#IBOutlet private var mContentView: UIView!
#IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
#IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!
func fittedSize(forConstrainedSize size: CGSize)->CGSize {
let fittedSize: CGSize!
//if height is greatest value, then it's dynamic, so it must be calculated
if size.height == CGFLoat.greatestFiniteMagnitude {
var height: CGFloat = 0
/*now here's where you want to add all the heights up of your views.
apple provides a method called sizeThatFits(size:), but it's not
implemented by default; except for some concrete subclasses such
as UILabel, UIButton, etc. search to see if the classes you use implement
it. here's how it would be used:
*/
height += mTitle.sizeThatFits(size).height
height += mDescription.sizeThatFits(size).height
height += mCustomView.sizeThatFits(size).height //you'll have to implement this in your custom view
//anything that takes up height in the cell has to be included, including top/bottom margin constraints
height += mTitleTopConstraint.constant
height += mDescriptionBottomConstraint.constant
fittedSize = CGSize(width: size.width, height: height)
}
//else width is greatest value, if not, you did something wrong
else {
//do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
}
return fittedSize
}
}
now make your controller conform to UICollectionViewDelegateFlowLayout, and in it, have this field:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}
it will be used as a prototype cell to bind data to and then determine how that data affected the dimension that you want to be dynamic
finally, the UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) has to be implemented:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
private var mDataSource: [CustomModel]
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize {
//bind the prototype cell with the data that corresponds to this index path
mCustomCellPrototype.bind(model: mDataSource[indexPath.row]) //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind
//define the dimension you want constrained
let width = UIScreen.main.bounds.size.width - 20 //the width you want your cells to be
let height = CGFloat.greatestFiniteMagnitude //height has the greatest finite magnitude, so in this code, that means it will be dynamic
let constrainedSize = CGSize(width: width, height: height)
//determine the size the cell will be given this data and return it
return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
}
}
and that's it. Returning the cell's size in collectionView(:layout:sizeForItemAt:) in this way preventing me from having to use estimatedItemSize, and inserting and deleting cells works perfectly.
In Swift 5, it works for me.
UICollectionViewFlowLayout:
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
UICollectionViewCell:
(ps: I'm using SnapKit)
class Cell: UICollectionViewCell {
let customizedContentView = UIView()
...
func layoutAction() {
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customizedContentView)
customizedContentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
then you just need to expand customizedContentView.