How to display large UICollectionView with autosizing cells in performant manner? - ios

I am trying to display a UICollectionView capable of displaying a user's photo library where the photo aspect ratio is respected (constrained by the width of the device - think the instagram full width feed view).
I have used examples from UICollectionView Self Sizing Cells with Auto Layout and https://medium.com/#wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2 (there is a PR in the repo linked in the article to update the project for iOS 12 FYI) to setup a UICollectionView with variable height cells, but all of these are super laggy/terrible to use when there are a large amount of cells in the view.
For example, the sample app (with iOS 12 fix) here: https://github.com/tttsunny/CollectionViewAutoSizingTest/pull/5 works great with 100 cells, but if you try to display 30,000 then the app is unusable - it gets stuck scrolling and is not responsive. The latency is in the estimated size logic of the UIViewController.
Is using an estimated size not recommended for large size UICollectionView objects? What can I do to get the cells to have different height values then?

While I can't speak to all large collection views, if you are seeing all your latency in the estimated size calculation, then sizing your cells a different way is likely to help a lot. Since you have a fixed width in this scenario, and simply need to calculate the height for each cell, you can accomplish all your size calculations off the main thread, which'll hopefully stop your scrolling getting suck and unresponsive.
More practically, for each "page" of images (I'd recommend starting with 20, but you can adjust this based on how it works in your situation), calculate the required cell heights for each. Do this in a background queue, by looking at the UIImage imageSize, and getting your cell height with: (imageSize.height / imageSize.width) * UIScreen.main.bounds.size.width. You'll need to keep these in memory, probably best alongside the images themselves, and then your sizeForItemAtIndexPath method can just index into that data structure for the cell's size. Then as your user scrolls, calculate the sizes for the "next page" before they get there (using either the collectionView prefetching API or simply hooking into the UIScrollView delegates.
You could even do this calculation in sizeForItemAtIndexPath directly, but calculating things in the background would be even more efficient. Calculating directly in this method could be an easier first step, though :).

Related

UITableview to always take full display height

is there a way in which a UITableView can always take a fixed height and can scale the static cells i'm using to the remaining height? I'm trying to achieve that my tableview layout is always visible on different devices.
I think I understand what you're getting at with having the table view showing on all devices. Instead of setting a specific size I'd recommend using auto layout instead! You can read more here.

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.

Scroll UITableView to bottom when using estimated row heights

I'm having a problem that seems like it shouldn't really be a problem. I'm trying to create a comments ("chat") view for my app, but I'm using estimated row heights and can't find a nice way to have the comments start at the bottom, such that when the view is loaded, the latest comment is at the bottom of the screen, just above the input area also at the bottom of the screen.
I've been looking around for ages and have found solutions like these:
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Doesn't work nicely because it uses the estimated row height, which isn't always right (some comments will be much longer).
[self.tblComments setContentInset:UIEdgeInsetsMake(self.tblComments.bounds.size.height - self.tblComments.contentSize.height, 0, 0, 0)];
Same problem as above (contentSize is determined by estimatedRowHeight).
I'm sure there's a perfect solution to this somewhere since so many apps have views like this, I just can't find it. I'd love any insight into something I may be missing here...
Edit:
contentSize is simply "The size of the content view." But since you've set a value for estimatedRowHeight, all your off-screen cells are assumed to be of height estimatedRowHeight.
According to the docs, here's the point of estimatedRowHeight:
Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
So basically, in order to save time, the table is making an assumption about row height and thus content size based on your estimatedRowHeight. By using estimatedRowHeight you're asking your UITableView to make these "shortcuts" in order to improve performance. You're basically telling the program not to calculate the row heights ahead of time, so in order to get the row heights and content sizes of any off-screen content, you have to calculate them manually. As long as you decide to set an estimatedRowHeight, this is unavoidable.
My guess is that many chat apps you've looked at don't use estimatedRowHeight in order to avoid this issue...
But if you do in fact feel as if using estimatedRowHeight helps with your app's performance, here are two suggestions.
SUGGESTION #1
Perhaps it makes sense to have a totalChatHeight variable where you keep track of the total row heights as they come in, calculated using similar logic as your heightForRowAtIndexPath: method. By originally calculating this info in the background as the comments load then incrementing as you go, you'd still maintain the performance benefits of using estimatedRowHeight without sacrificing the functionality of having the table know its row heights.
By doing this, you can structure your contentOffset statement like so to scroll to the bottom using the known table row height information:
CGPoint bottomOffset = CGPointMake(0, totalChatHeight - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
SUGGESTION #2
Another suggestion would be to calculate your row average dynamically so you have a more precise measurement for estimatedRowHeight and the contentSize of your UITableView would approximately be the same with or without using estimatedRowHeight. That way estimatedRowHeight is actually the estimate row height and the estimated content size should also be near the actual contentSize, so this standard contentOffset formula should work:
CGPoint bottomOffset = CGPointMake(0, self.tblComments.contentSize.height - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
This could potentially be dangerous though as both the chat log and rounding error grow.
SUGGESTION #3 <-- Perhaps the best way to go in this case
This third suggestion may be the easiest to implement in your case as it doesn't require any ongoing calculations:
Just set estimatedRowHeight to the last row's height before reloading the table's data.
You can calculate that new row's height using whatever algorithm you use in heightForRowAtIndexPath: then set estimatedRowHeight to that height; that way you can use this statement to scroll to the last cell
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
and even though, as you said, it determines the row position based on estimatedRowHeight, it should scroll to the correct position as long as the estimatedRowHeight equals the bottom row height.

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.

Resources