Setting UICollectionViewCellSize at runtime - ios

I have a stack view that contains three UICollectionViews, set up to give each of them equal vertical space. That stack view is set to be the height of half of the display, so that it uses more space on larger devices. This has been set up in Interface Builder.
So, I need to set the cell size of the UICollectionView at runtime, since until we are running, I don't know what the actual size of the cells will be. I want them to be square, so I just want to take into account the height of the UICollectionView, subtract out the top and bottom section insets, and set the itemSize to the resulting size.
I attempt to do this in viewDidLayoutSubviews, since by then I figure that the initial heights of the collection views have been set. However, they appear to be set to 1000x1000 (even though they are a much more reasonable size in the storyboard), and so I compute a cell size based on a collection view height of 1000. This is too large, but I figure that I'll get called again and get another chance to recompute it. And I do, but not before UICollectionView complains loudly that the itemSize is incorrect (ie. too large to fit in the collectionView, which now has the "correct" size.)
What is the best way to get the behavior I'm looking for without the warnings from UICollectionView? Setting the collection view item size at runtime based on the eventual size of the UICollectionView is something I've struggled with in the past, and there never seems to be the "right" time to set the itemSize. I don't want to dynamically return it, if only because it's not something that changes during the life of the program. There just seems to be some inconsistencies that occur when laying out the views initially.
It seems odd to me that the collection view comes in with an initial size of 1000.0 by 1000.0, but I'm not sure how or why to fix that - perhaps it has something to do with being embedded in a stack view?
Edited to add: It is almost certainly the UIStackView that is causing the layout issues. I created a dummy project to test the size of a UICollectionView when it is the top level view vs embedded in a UIStackView. If it is not embedded, when viewDidLayoutSubviews is called, it has been properly sized to fit the bounds of its superview. However, if it is inside of a UIStackView, it stays at the default size of 1000x1000.
For now, I am working around this problem by adding the following code in viewDidLayoutSubviews:
if collectionView.bounds.size.width > view.bounds.size.width {
view.layoutIfNeeded()
}
Where collectionView is inside a UIStackView and view is the main view of the UIViewController. This allows all subsequent calculations based on the size of the view to be correct, and hopefully will not get called if the UIStackView behavior ever gets fixed.
Similar discussions here and here. Interesting point that in XCode8, the new default is to not save sizes of views in the XIB file, but instead bring everything in with an initial size of 1000x1000, to be resolved during the first layout pass. Except for UIStackViews, I guess.

Are you having sizeForItemAtIndexPath() return itemSize? I have found that implementing that function is the only reliable way to size a UICollectionViewCell dynamically at runtime.

Related

Hide arrangedSubviews in UIStackView instead of clipping contents

I have a UIStackView with three labels whose height is determined using Dynamic Type and text that have can wildly varying lengths. The container for the stack view has a fixed width and height depending on device screen size (small on iPhone SE, for example.) I want to center the stack view within the container (with some outer margins.)
The problem is that depending on the font size and container height, some of the labels in the stack view will be clipped. Here is an example with the third label:
I have experimented with layout constraint priorities for both the stack view and the labels, but this doesn't appear to be the right approach. Instead setting the visibility of the labels works better: correct spacing between elements is maintained.
My question is then what is the right time to detect that the label's height isn't fully displayed and to hide it.
The label height is close to, but not exactly equal to the UIFont's lineHeight so there's some rounding involved that makes this a little difficult.
The biggest problem is that after a layout pass in the UIStackView's layoutSubviews the heights of the arranged subviews can be detected, but you can't hide the arranged views at that point because it causes another layout pass and recursion.
So what am I missing? :-)
Here's a test project - build for iPhone Xs in the Simulator and you'll see the same results in the screenshot above.
Solution
Tom Irving's gist below pointed me in the right direction. The trick is to enumerate the subviews after a layout pass and remove them if height requirements aren't met.
The updated project shows how to do this in DebugStackView's layoutSubviews. And yes, UIStackView is a worthy adversary.
Could you act on viewDidLoad?
My intuition would be to add up the height of all visible subviews in the stack view and then hide the last if there's a problem.
In the sample you've provided I would recommend getting a CGSize with [self.firstLabel textRectForBounds:self.view.bounds limitedToNumberOfLines:0] for each visible label, making sure to take the margin between items into acount, and determining if the total height is greater than the constant height you've assigned to the stack view. If so, hide the elements that go beyond the stack view's height.
Of course, there might be more to the problem than I understand, but that would allow you to calculate before the layoutSubview pass happens.

collectionViewContentSize() vs contentSize

What is the difference between
collectionViewController.collectionViewLayout.collectionViewContentSize() and collectionViewController.collectionView.contentSize ?
What do you prefer to use?
contentSize is a property of UIScrollView, whereas collectionViewContentSize() is a method of UICollectionViewLayout.
Reading Programming iOS 7, 4th Edition, whilst summarising UICollectionViewLayout, the author states:
The layout workhorse class for a collection view. A collection view
cannot exist without a layout instance! As I’ve already said, the
layout knows how much room all the subviews occupy, and supplies the
collectionViewContentSize that sets the contentSize of the collection
view, qua scroll view.
From personal experience I encountered difficulties using layout.collectionViewContentSize(). Either the console showed the warning below, or the layouts appeared initially incorrect.
the behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values.
My guess is that collectionViewContentSize() does more than just return the content size. I think that invoking this actually initiates the layout calculations. If - during those calculations - anomalies are detected, then the warning shown is output.
For me testing on iPad and initiating the collection view via a Xib, the Xib's frames were iPhone sized. Initial passes triggered from viewDidLayoutSubviews were dealing with the small view. Checking the collection view's frame, it was too small. Subsequent layout engine passes eventually delivered the correct sizes, but by then the warnings had already been displayed.
Next, I tried making the Xib frame much larger. This obviated the error, but caused a worse problem; layouts were initially wrong. In scrolling, you would see the items jump around as the layout was recalculated to the correct dimensions.
Possibly the answer is to call invalidateLayout(), but I'm not sure where. I tried after instantiating the collection view and that didn't work.
So, the answer? I used contentSize. Initial passes in viewDidLayoutSubviews still show incorrect sizes but eventually come good. It doesn't generate any console warnings and better still, doesn't trigger any erroneous layout based upon the old frame size. Even better, rotation is handled automatically as viewDidLayoutSubviews will be called automatically by which point the content size will have been updated.
collectionViewContentSize() is a method you can override (in the layout) to generate the size dynamically.
contentSize is a property of collectionView (or any UIScrollView, for that matter) that is going to be used if there is no such override.
It is similar to a UITableView's rowHeight vs. the UITableViewDelegate's heightForRowAtIndexPath().
Please also note (as mentioned in the comments) that "contentSize is a UIScrollView-inherited property, while itemSize is the property on UICollectionViewLayout that allows you to specify a blanket size for each cell".

Sizing a UICollectionView to fit its contents within an AutoLayout View

I have a UIView that I'm using as a footer for a UITableView. Within this UIView Is a UIWebView and UICollectionView. They are laid out as follows:
H:|[webview]|
H:|[collection]|
V:|[webview]-8-[collection(10#1)]|
This is being done via IB but this indicates the constraints that are in place.
I'd like to have the webview sized to it's contents (I have that working) and the collection as well sized to it's contents. I'd like the overall UIView they are contained in sized to fit all of this without scrolling as it's contained in a tableview which handles it's own scrolling.
I can not seem to get the collection to size itself larger than the 10 points that the constraint has. In addition the constraint system demands that something have a height.
How do I get to my goal of a view sized to fit both subviews which are in turn sized to fit their contents? I have tried adjusting the frame on the collectionview in the sizeToFit method for the overall view but I believe the constraint system is undoing that.
Collection views have no intrinsic content size associated with them. There is currently no way to achieve what you are looking for.
If you feel that this is something that would be useful to have, please file an enhancement request at http://bugreport.apple.com (log in with your developer account).

iOS Autolayout intrinsicContentSize = -1

When using autolayout, calling intrinsicContentSize seems to be the method to determine what CGSize is required to properly fit the views content.
However, this method is only supported for a limited number of existing UIViews.
Anytime that I make a custom view, even if it is something as simple as a UILabel inside of a container UIView, that containing view is unable to determine its intrinsicContentSize (returns -1).
I don't understand why the view is able to properly be displayed on the screen yet the view doesn't even know its own height...
The UILabel in a container view is a simple example but I'm dealing with slightly more complicated UIViews where there are maybe 15 views nested within eachother. In order to determine the size of the view which contains all of its subviews, I have to manually create my own intrinsicContentSize method and do very time consuming work where I have to sum up all the heights of the subviews plus add to that all of the constraints.
This process is terrible. It's very easy to miss out on a height somewhere by forgetting to add the height of one of the subviews or constraints. Also, the matter is further complicated by the fact that with dynamic subviews. For example, if the view has 2 columns of dynamic subviews, you need to manually find the height of the subviews+constraints for each column, compare these heights and return the larger of the two. Again, this is a simple example but often it's not so simple, causing many many migraines.
Back to what I was asking earlier. How can iOS display the view yet not even know how tall the view is? There must be some way to find out what that height is. Thanks for reading.
Here is an image to help visualize what I want.
Are all your subviews using auto-layout themselves? I mean that if your using auto-layout to place MyCompositeObject, is that composite object using constraints internally to place its many objects? I've found that if so, then the intrinsicContentSize will account for all the subviews, but if not, your UIView's intrinsic content size is going to end out returning something inaccurate and small.

Why is AutoLayout not taking care of my UICollectionView

I have built a very simple sample of an app (Source code on github) using a UICollectionView.
Everything is working fine, as long as the app is in portrait mode. When it is changed to landscape mode however, the content cell is not resized appropriately, and thus, nothing is displayed.
I thought that all the necessary AutoLayout constraints are in place. I am aware that I can implement collectionView:layout:sizeForItemAtIndexPath:, but my goal is to use AutoLayout as much as possible (simply to understand AutoLayout better).
What am I missing here?
You can use autolayout to set the position and size of the collection view. And you can use autolayout to set the position and size of the subviews inside each cell. But you cannot use autolayout to control the position and size of the cells. You must use the collection view's layout object to set the position and size of each collection view cell. If you want the cells to change size when the interface orientation changes, you must update your layout object to report the new size and invalidate the layout.

Resources