Hi i want to get the subviews height to estimate the detailUIView heightAnchor. In the detailUIView i have 4 labels that layedout using auto constraints(programmatically) and i want to get the views height so i can estimate the detailUIView.In the function estimateCardHeight i am iterating all the subviews and trying to get the height of each subview and adding them but i am getting 0.0. Whats am i missing here i couldn't figure it out.
class ComicDetailViewController: UIViewController {
var comicNumber = Int()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = "Comic Number: \(comicNumber)"
}
let detailUIView = DetailComicUIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Do any additional setup after loading the view.
detailUIView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(detailUIView)
addConstraints()
print(estimateCardHeight())
}
public func configure(with viewModel: XKCDTableViewCellVM){
detailUIView.configure(with: viewModel)
comicNumber = viewModel.number
}
private func estimateCardHeight() -> CGFloat{
var allHeight : CGFloat = 0
for view in detailUIView.subviews{
allHeight += view.frame.origin.y
}
return allHeight
}
private func addConstraints(){
var constraints = [NSLayoutConstraint]()
constraints.append(detailUIView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10))
constraints.append(detailUIView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 20))
constraints.append(detailUIView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -20))
constraints.append(detailUIView.heightAnchor.constraint(equalToConstant: self.view.frame.height/3))
NSLayoutConstraint.activate(constraints)
}
}
you are missing view.layoutIfNeeded()
addConstraints()
view.layoutIfNeeded()
print(estimateCardHeight())
as apple's saying
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints.
Lays out the subviews immediately, if layout updates are pending.
And I guess:
the way to estimate height,
private func estimateCardHeight() -> CGFloat{
var allHeight : CGFloat = 0
for view in detailUIView.subviews{
allHeight += view.frame.size.height
// allHeight += view.frame.origin.y
}
return allHeight
}
An other way to achieve it.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(estimateCardHeight())
}
viewDidLayoutSubviews()
Called to notify the view controller that its view has just laid out its subviews.
I'm trying to follow the example described here for making a stretchy layout which includes a UIImageView and UIScrollView. https://github.com/TwoLivesLeft/StretchyLayout/tree/Step-6
The only difference is that I replace the UILabel used in the example with the view of a child UIViewController which itself contains a UICollectionView. This is how my layout looks - the blue items are the UICollectionViewCell.
This is my code:
import UIKit
import SnapKit
class HomeController: UIViewController, UIScrollViewDelegate {
private let scrollView = UIScrollView()
private let imageView = UIImageView()
private let contentContainer = UIView()
private let collectionViewController = CollectionViewController()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.delegate = self
imageView.image = UIImage(named: "burger")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let imageContainer = UIView()
imageContainer.backgroundColor = .darkGray
contentContainer.backgroundColor = .clear
let textBacking = UIView()
textBacking.backgroundColor = #colorLiteral(red: 0.7450980544, green: 0.1235740449, blue: 0.2699040081, alpha: 1)
view.addSubview(scrollView)
scrollView.addSubview(imageContainer)
scrollView.addSubview(textBacking)
scrollView.addSubview(contentContainer)
scrollView.addSubview(imageView)
self.addChild(collectionViewController)
contentContainer.addSubview(collectionViewController.view)
collectionViewController.didMove(toParent: self)
scrollView.snp.makeConstraints {
make in
make.edges.equalTo(view)
}
imageContainer.snp.makeConstraints {
make in
make.top.equalTo(scrollView)
make.left.right.equalTo(view)
make.height.equalTo(imageContainer.snp.width).multipliedBy(0.7)
}
imageView.snp.makeConstraints {
make in
make.left.right.equalTo(imageContainer)
//** Note the priorities
make.top.equalTo(view).priority(.high)
//** We add a height constraint too
make.height.greaterThanOrEqualTo(imageContainer.snp.height).priority(.required)
//** And keep the bottom constraint
make.bottom.equalTo(imageContainer.snp.bottom)
}
contentContainer.snp.makeConstraints {
make in
make.top.equalTo(imageContainer.snp.bottom)
make.left.right.equalTo(view)
make.bottom.equalTo(scrollView)
}
textBacking.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
collectionViewController.view.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.scrollIndicatorInsets = view.safeAreaInsets
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.safeAreaInsets.bottom, right: 0)
}
//MARK: - Scroll View Delegate
private var previousStatusBarHidden = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if previousStatusBarHidden != shouldHideStatusBar {
UIView.animate(withDuration: 0.2, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
previousStatusBarHidden = shouldHideStatusBar
}
}
//MARK: - Status Bar Appearance
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return shouldHideStatusBar
}
private var shouldHideStatusBar: Bool {
let frame = contentContainer.convert(contentContainer.bounds, to: nil)
return frame.minY < view.safeAreaInsets.top
}
}
Everything is the same as in this file: https://github.com/TwoLivesLeft/StretchyLayout/blob/Step-6/StretchyLayouts/StretchyViewController.swift with the exception of the innerText being replaced by my CollectionViewController.
As you can see, the UICollectionView is displayed properly - however I am unable to scroll up or down anymore. I'm not sure where my mistake is.
It looks like you are constraining the size of your collection view to fit within the bounds of the parent view containing the collection view's container view and the image view. As a result, the container scrollView has no contentSize to scroll over, and that's why you can't scroll. You need to ensure your collection view's content size is reflected in the parent scroll view's content size.
In the example you gave, this behavior was achieved by the length of the label requiring a height greater than the height between the image view and the rest of the view. In your case, the collection view container needs to behave as if it's larger than that area.
Edit: More precisely you need to pass the collectionView.contentSize up to your scrollView.contentSize. A scrollview's contentSize is settable, so you just need to increase the scrollView.contentSize by the collectionView.contentSize - collectionView.height (since your scrollView's current contentSize currently includes the collectionView's height). I'm not sure how you are adding your child view controller, but at the point you do that, I would increment your scrollView's contentSize accordingly. If your collectionView's size changes after that, though, you'll also need to ensure you delegate that change up to your scrollView. This could be accomplished by having a protocol such as:
protocol InnerCollectionViewHeightUpdated {
func collectionViewContentHeightChanged(newSize: CGSize)
}
and then making the controller containing the scrollView implement this protocol and update the scrollView contentSize accordingly. From your collectionView child controller, you would have a delegate property for this protocol (set this when creating the child view controller, setting the delegate as self, the controller containing the child VC and also the scrollView). Then whenever the collectionView height changes (if you add cells, for example) you can do delegate.collectionViewContentHeightChanged(... to ensure your scroll behavior will continue to function.
I am trying to make a circular view which has an adaptive size based on auto layout, currently i set the constraints, then i attempt to round the image in the viewwilllayoutsubviews method.
This is resulting in oddly shaped views that are not circular, how can i resolve this?
init:
profilePic = UIImageView(frame: CGRect.zero)
profilePic.clipsToBounds = true
profilePic.contentMode = .scaleAspectFill
constrains:
profilePic.snp.makeConstraints { (make) -> Void in
make.centerX.equalTo(self).multipliedBy(0.80)
make.centerY.equalTo(self).multipliedBy(0.40)
make.size.equalTo(self).multipliedBy(0.22)
}
subviews:
override func viewWillLayoutSubviews() {
self.navigationMenuView.profilePic.layer.cornerRadius = self.navigationMenuView.profilePic.frame.size.width / 2.0
self.navigationMenuView.profilePic.layer.borderWidth = 2
self.navigationMenuView.profilePic.layer.borderColor = UIColor.white.cgColor
}
result:
I guess you want this (sorry for the plain autolayout, but I don't use snapkit):
profilePic.heightAnchor.constraint(equalTo: profilePic.widthAnchor).isActive = true
profilePic.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.22).isActive = true
Instead of this:
make.size.equalTo(self).multipliedBy(0.22)
I had the same problem
This is my solution:
let profilePicHeight: CGFloat = 30.0
Add this line of code to your constrains:
profilePic.snp.makeConstraints { (make) -> Void in
make.height.width.equalTo(self.profilePicHeight)
...
}
then:
override func viewWillLayoutSubviews() {
self.navigationMenuView.profilePic.layer.cornerRadius = self.profilePicHeight / 2.0
...
}
My suggestion here is don't treat it like a circular view from the outside of it. Make the view itself conform to being a circle so that you can use it anywhere.
INSIDE the view give it constraints like...
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
This will make it square (with undetermined size).
Then in the function layoutSubviews...
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.width * 0.5
}
This will make the square into a circle.
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