collectionViewContentSize() vs contentSize - ios

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".

Related

Setting UICollectionViewCellSize at runtime

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.

sizeThatFits on view with constraints (auto layout view inside frame layout view)

View A layouts it's subviews by calculating and setting their frames in layoutSubviews.
View B uses constraints to layout it's subviews.
View A contains a number of View B instances.
View A needs to know the size of its subviews when it layouts them, so it calls sizeThatFits: on them.
View B's height depends on it's width. In my example it has a multiline UILabel inside it.
How should B implement it's sizeThatFits: so it returns the correct size taking both the size parameter passed to sizeThatFits: and the constraints in account?
What I've tried/considered so far:
systemLayoutSizeFittingSize: - doesn't work because it doesn't take into account an exact size passed to it. It can't be used to specify an exact width for e.g a view that grows vertically as it shrinks horizontally. It only gives either the most compressed size or the most expanded size.
intrinsicContentSize- this is used to pass information to Auto Layout as far as i understand from the docs. What we want is to get information from Auto Layout.
This is how i've reasoned so far. Please correct me if i've made a mistake. And please help me, i'd be very grateful if someone could point me to how this is done!
edit: Also, maybe there is some entirely different way to do this, without even using sizeThatFits:? The only requirement i have is that i can continue using frame layout in the "outer" view (View A) and auto layout in the "inner view" (View B).
edit2: Added a picture to illustrate the exact case i must solve
edit3: Forgot to mention, i need to support iOS 7.0
From the picture it would appear that a UIStackView might be a better approach for View A?
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/
and then just constrain with a margin offset the bottom of the View A UIStackView to the bottom of the last View B
I would use Autolayout for the lot. You can still specify exact sizes and positions, you just do it with constraints instead of with frames.

Is it OK to modify the `intrinsicContentSize` method for `UITableView` and `UICollectionView`?

I want to use UITableView and UICollectionView with autolayout. However since they don't have the intrinsicContentSize method, it is hard to set constraint for all the views.
Is it OK if I modify the intrinsicContentSize method of them to return the contentSize for UITableView and self.collectionViewLayout.collectionViewContentSize for UICollectionView?
Regarding intrinsicContentSize the documentation states that:
Overriding this method allows a custom view to communicate to the
layout system what size it would like to be based on its content.
As UITableView and UICollectionView are both subclasses of the UIView, this method must work for them too. I don't see any cautions in the docs about using it with them.
You can also check out this great tutorial concerning Auto Layout, and Intrinsic Content Size in particular. Pay attention to the following:
To implement an intrinsic content size in a custom view, you have to
do two things: override intrinsicContentSize to return the appropriate
size for the content, and call invalidateIntrinsicContentSize whenever
something changes which affects the intrinsic content size.
So don't forget to call invalidateIntrinsicContentSize when your table view reloads data, inserts new rows etc. And the same applies to the UICollectionView.
Hope this helps.

AutoLayout: UIView within UIView has incorrect frame

I am working with AutoLayout in Storyboard, and everything seemed to be going fine. However, when I put one UIView within another and apply all the constraints I want to both the container and its children, I notice that the frame is incorrect in viewDidLayoutSubviews:. Rather than being the frame I would expect to be calculated given my constraints, it is some disgustingly large frame (albeit at the correct origin). For example, rather than a frame of {{26, 10}, {444, 10}} I get something like {{0, 0}, {320, 568}}.
Strangely enough, this only happens to the child UIView when I situate it within another UIView that has some constraints applied to it in relation to its superview (which is the view controller's view). As I am under the impression that I can expect my views to be properly laid out according to my constraints in the method viewDidLayoutSubviews:, I am confused as to why this is happening.
Am I making any incorrect assumptions? I would appreciate it if someone might help point me in the right direction. Thanks!
Note: This problem is completely fixed if I do my correct-frame-dependent setup in viewDidAppear:, but this is a somewhat unsatisfying workaround for me.
Auto Layout changes to bounds are not necessarily finished in -viewDidLayoutSubviews. As put in "Advance Auto Layout Toolbox":
Constraint-based layout is an iterative process. The layout pass can
make changes to the constraints based on the previous layout solution,
which again triggers updating the constraints following another layout
pass.
The documentation for -viewDidLayoutSubviews notes with emphasis in the original:
However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted.
So layout and constraint-based iterations in the subviews could continue to adjust their frames, but if a ViewController's view's bounds don't change, its -viewWillLayoutSubviews won't get called.
Since you say your setup is "frame-dependent" you have a couple options.
1. Do your setup in -viewWillAppear. Better than -viewDidAppear, and since you really need everything to be done after all the layout if completed but before it appears on screen, this is a legitimate reason why the method is there. If relevant, you could try calling -isMovingToParentViewController
as described in Determining Why A View's Appearance Changed.
EDIT: -viewWillAppear is called before the view is added to the view hierarchy and therefor layout may not be completed. Sorry everyone, and thankfully with Swift UI we are moving away from procedural layout to declarative layout, and this is why.
Call -layoutIfNeeded on the views you need to have the correct values where you need them to. Because this does the layout work it's expensive, and it could lead to repetitive work being done or even an infinite loop if triggered during its own layout process. But it's useful if it's needed to sync some values with Auto Layout so they can be used.
If this doesn't work let us know, it could have to do with the nature of the constraints themselves or something else. The Auto Layout iterations can be tricky to sync with. Good luck.

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.

Resources