UICollectionView performance - _updateVisibleCellsNow - ios

I'm working on a custom UICollectionViewLayout that displays cells organized by day/week/month.
It is not scrolling smooth, and it looks like the lag is caused by [UICollectionView _updateVisibleCellsNow] being called on each rendering loop.
Performance is OK for < 30 items, but at around 100 or more, its terribly slow. Is this a limitation of UICollectionView and custom layouts, or am I not giving the view enough information to perform correctly?
Source here: https://github.com/oskarhagberg/calendarcollection
Layout: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarWeekLayout.m
Data source and delegate: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarView.m
Time Profile:
Update
Maybe its futile? Some testing with a plain UICollectionViewController with a UICollectionViewFlowLayout that is given approximately the same amount of cells/screen results in a similar time profile.
I feel that it should be able to handle ~100 simple opaque cells at a time without the jitter. Am I wrong?

Also don't forget to try to rasterize the layer of the cell:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
I had 10 fps without that, and boom 55fps with!
I'm not really familiar with GPU and compositing model, so what does it do exactly is not entirely clear to me, but basically it flatten the rendering of all subviews in only one bitmap (instead of one bitmap per subview?).
Anyway I don't know if it has some cons, but it is dramatically faster!

I have been doing considerable investigation of UICollectionView performance. The conclusion is simple. Performance for a large number of cells is poor.
EDIT: Apologies, just re-read your post, the number of cells you have should be OK (see the rest of my comment), so cell complexity may also be a problem.
If your design supports it check:
Each cell is opaque.
Cell content clips to bounds.
Cell coordinate positions do not contain fractional values (e.g. always calculate to be whole pixels)
Try to avoid overlapping cells.
Try to avoid drop shadows.
The reason for this is actually quite simple. Many people don't understand this, but it is worth understanding: UIScrollViews do not employ Core Animation to scroll. My naive belief was that they involved some secret scrolling animation "sauce" and simply requested occasional updates from delegates every now and then to check status. But in fact scroll views are objects which don't directly apply any drawing behaviour at all. All they really are is a class which applies a mathematical function abstracting the coordinate placement of the UIViews they contain, so the Views coordinates are treated as relative to an abstract contentView plane rather than relative to the origin of the containing view. A scroll view will update the position of the abstract scrolling plane in accord with user input (e.g. swiping) and of course there is a physics algorithm as well which gives "momentumn" to the translated coordinate positions.
Now if you were to produce your own collection view layout object, in theory, you could produce one which 100% reverses the mathematical translation applied by the underlying scrollview. This would be interesting but useless, because it would then appear that the cells are not moving at all as you swipe. But I raise this possibility because it illustrates that the collection view layout object working with the collection view object itself does a very similar operation to the underlying scrollview. E.g. it simply provides an opportunity to apply an additional mathematical frame by frame translation of the attributes of the views to be displayed, and in the main this will be a translation simply of position attributes.
It is only when new cells are inserted or deleted moved or reloaded that CoreAnimation is involved at all; most usually by calling:
- (void)performBatchUpdates:(void (^)(void))updates
completion:(void (^)(BOOL finished))completion
UICollectionView requests cell layoutAttributes for each frame of scrolling and each visible view is laid out for each frame. UIView's are rich objects optimised for flexibility more than performance. Every time one is laid out, there are a number of tests the system does to check it's alpha, zIndex, subViews, clipping attributes etc. The list is long. These checks and any resulting changes to the view are being conducted for each collection view item for each frame.
To ensure good performance all frame by frame operations need to be completed within 17ms. [With the number of cells you have, that is simply not going to happen] bracketed this clause because I have re-read your post and I realise I had misread it. With the number of cells you have, there should not be a performance problem. I have personally found with a simplified test with vanilla cells, containing only a single cached image, the limit tested on an iPad 3 is about 784 onscreen cells before performance starts to drop below 50fps.
In practice you will need to keep it less than this.
Personally I'm using my own custom layout object and need higher performance than UICollectionView provides. Unfortunately I didn't run the simple test above until some way down the development path and I realised there are performance problems. I'm so I'm going to be reworking the open source back-port of UICollectionView, PSTCollectionView. I think there is a workaround that can be implemented so, just for general scrolling about, each cell item's layer is written using an operation which circumvents the layout of each UIView cell. This will work for me since I have my own layout object, and I know when layout is required and I have a neat trick that will allow the PSTCollectionView to fall back to its normal mode of operation at this time. I've checked the call sequence and it doesn't appear to be too complex, nor does it appear at all unfeasible. But for sure it is non-trivial and some further tests have to be done before I can confirm it will work.

Some more observations that might be helpful:
I am able to reproduce the problem, using flow layout with around 175 items visible at once: it scrolls smoothly in the simulator but lags like hell on iPhone 5. Made sure they are opaque etc.
What ends up taking the most time seems to be work with a mutable NSDictionary inside _updateVisibleCellsNow. Both copying the dictionary, but also looking up items by key. The keys seems to be UICollectionViewItemKey and the [UICollectionViewItemKey isEqual:] method is the most time consuming method of all. UICollectionViewItemKey contains at least type, identifier and indexPath properties, and the contained property indexPath comparison [NSIndexPath isEqual:] takes the most time.
From that I'm guessing that the hash function of UICollectionViewItemKey might be lacking since isEqual: is called so often during dictionary lookup. Many of the items might be ending up with the same hash (or in the same hash bucket, not sure how NSDictionary works).
For some reason it is faster with all items in 1 section, compared to many sections with 7 items in each. Probably because it spends so much time in NSIndexPath isEqual and that is faster if the row diffs first, or perhaps that UICollectionViewItemKey gets a better hash.
Honestly it feels really weird that UICollectionView does that heavy dictionary work every scroll frame, as mentioned before each frame update needs to be <16ms to avoid lag. I wonder if that many dictionary lookups either is:
Really necessary for general UICollectionView operation
There to support some edge case rarely used and could be turned off for most layouts
Some unoptimized internal code that hasn't been fixed yet
Some mistake from our side
Hopefully we will see some improvement this summer during WWDC, or if someone else here can figure out how to fix it.

Here is Altimac's answer converted to Swift 3:
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
Also, it should be noted that this code goes in your collectionView delegate method for cellForItemAtIndexPath.
One more tip - to see an app's frames per second (FPS), open up Core Animation under Instruments (see screenshot).

The issue isn't the number of cells you're displaying in the collection view total, it's the number of cells that are on screen at once. Since the cell size is very small (22x22), you have 154 cells visible on screen at once. Rendering each of these is what's slowing your interface down. You can prove this by increasing the cell size in your Storyboard and re-running the app.
Unfortunately, there's not much you can do. I'd recommend mitigating the problem by avoiding clipping to bounds and trying not to implement drawRect:, since it's slow.

Big thumbs up to the two answers above!
Here's one additional thing you can try: I've had big improvements in UICollectionView performance by disabling auto layout. While you will have to add some additional code to layout the cell interiors, custom code seems to be tremendously faster than auto layout.

Beside the listed answers (rasterize, auto-layout, etc.), you may also want to check for other reasons that potentially drags down the performance.
In my case, each of my UICollectionViewCell contains another UICollectionView (with about 25 cells each). When each of the cell is loading, I call the inner UICollectionView.reloadData(), which significantly drags down the performance.
Then I put the reloadData inside the main UI queue, the issue is gone:
DispatchQueue.main.async {
self.innerCollectionView.reloadData()
}
Carefully looking into reasons like these might help as well.

In few cases it is due to Auto-layout in UICollectionViewCell. Turn it off (if you can live without it) and scrolling will become butter smooth :)
It's an iOS issue, which they havnt resolved from ages.

If you are implementing a grid layout you can work around this by using a single UICollectionViewCell for each row and add nested UIView's to the cell. I actually subclassed UICollectionViewCell and UICollectionReusableView and overrode the prepareForReuse method to remove all of the subviews. In collectionView:cellForItemAtIndexPath: I add in all of the subviews that originally were cells setting their frame to the x coordinate used in the original implementation, but adjusting it's y coordinate to be inside the cell. Using this method I was able to still use some of the niceties of the UICollectionView such as targetContentOffsetForProposedContentOffset:withScrollingVelocity: to align nicely on the top and left sides of a cell. I went from getting 4-6 FPS to a smooth 60 FPS.

Thought I would quickly give my solution, as I faced a very similar issue - image-based UICollectionView.
In the project I was working in, I was fetching images via network, caching it locally on device, and then re-loading the cached image during scrolling.
My flaw was that I wasn't loading cached images in a background thread.
Once I did put my [UIImage imageWithContentsOfFile:imageLocation]; into a background thread (and then applied it to my imageView via my main thread), my FPS and scrolling was a whole lot better.
If you haven't tried it yet, definitely give a go.

Related

How to animate UITableView rows when IndexPathsForVisibleRows[0] is moved or removed

I am experimenting with the UITableView, but animations are not occurring as I would expect.
Given a list of cells, I want to delete the top-most cell (the item at IndexPathsForVisibleRows[0]), and have all the rows beneath it, animate upwards into their new position.
However, it appears that any time IndexPathsForVisibleRows[0] (or any invisible cell ABOVE the first visible one for that matter) is touched (moved, deleted, etc), the UITableView refuses to animate the results of the transaction.
I have included 2 gifs to demonstrate the issue.
The first GIF demonstrates the desired behavior. I am removing the cell at IndexPathsForVisibleRows[1] (the second on-screen cell).
Notice how all cells below animate correctly into position.
The second GIF demonstrates what happens when removing the top-most cell (IndexPathsForVisibleRows[0]). Note how all the cells below move immediately into the new position, without animation.
(note: the entire table change is wrapped in a BeginUpdates/EndUpdates block).
Am I missing something which causes the 2 scenarios to behave differently, or I have I just stumbled upon a UITableView bug/limitation?
After much investigation, including several new code projects distilling the UITableView down to the various different factors that could be contributing to this behavior, I have found the simple answer.
Don't use the UITableView
I, like many others, have relied heavily on the UITableView in many applications, due to its simplicity and performance, however, if you really want it to behave in fluid, sensible ways, it just cannot deliver.
There are some inherent bugs in it's behavior which don't appear to bet getting addressed by Apple - and not surprisingly, when there is a much better alternative already in the SDK.
Enter the UICollectionView.
I took all my backend logic for and adapted it to supply a UICollectionView instead (all the work took about half an hour), and lo and behold, everything just worked as originally intended.
So, all my code was correct, the UITableView was just getting in the way.
This is how it now looks. Notice have all deletion scenarios animate correctly, no jankiness.
I'm not the first to have found UITableView lacking:
https://pspdfkit.com/blog/2017/the-case-for-deprecating-uitableview/
Additional benefits to UICollectionView:
customizable layouts
easily customizable cell animations
update transaction does not halt currently running scroll animations (this one is great, if you have a background thread refreshing data, you won't get a sudden 'jerk' when rows get rearranged)

Delay between viewWillDisappear and viewDidDissapear

I'm seeing some performance degradation in my application after some time and I'm trying to figure out what's going exactly.
I have a complex view controller (VC1) which contains scroll view, few table views inside, some custom cells with horizontal scrolling and custom drawing etc.
After several (around 10) refreshes of all these objects (reloading tables, reposition subviews etc) when I try to call presentViewController to push another view controller above VC1 I can see about 2 seconds delay between viewWillDisappear and viewDidDissapear
I tried to profile the app to see if there are memory leaks but couldn't find any. Memory usage grows when view refreshes and switches between different modes, but then it become more or less stable in around 30m.
Works fine in Simulator, but visible slower on iPhone5. And this slowness is visible only when I try to switch from that view controller.
I ran a profiler and recorded where these 2 seconds are spent. Here is link to trace file: https://dl.dropboxusercontent.com/u/6402890/trace.trace.zip
Majority of the time spent by UIKit doing layout as I can see.
What can I do to optimize it? Is there way to take may be a snapshot of a view and use it for "leaving view" animation and restore view hierarchy when we're coming back?
UPDATE: Adding screenshot for the profiler (click for full resolution):
UPDATE2:
After analyzing output from recursiveDescription I can see the following:
In the easiest case I have ~200 lines in the output. And performance is ok.
When I switch to more complex scenario hierarchy of views growth to ~500 lines, but still performs ok.
After multiple refreshes this number goes to ~2000 and this is where it become slow. Analyzing output with 2000 views I can see that ~1500 of them belong to hidden cells that are not even displayed in this mode anymore. When I'm refreshing table views cell types change too, and I'm utilizing different cells, but why the cells that are not used anymore are still being in subviews of table views?
Any recommendations?
From your stack, I suspect you've added a large number of views you didn't mean to add. Since it's related to reloads, I would check your reload logic and make sure it doesn't re-add all the views in your hierarchy without removing the previous views. You can write a quick debug routine use -recursiveDescription to recursively walk the -subviews of each view and print them out to see what's in the hierarchy.
It's possible that your issue is in the layer hierarchy rather than the view hierarchy, but the symptoms you describe make me think views.
EDIT: From your update, you probably have one of two things going on. Most likely, if these are actual UITableViewCells that shouldn't even exist anymore, then you have a retain loop somewhere. Alternately, your cellForRowAtIndexPath: may be incorrect and may be adding new views to an existing cell when it should just be reconfiguring the cell.
In either case, though, 200 views seems a lot of views for a "best case." You may be overusing views in places that you should be doing custom drawing. If the performance is ok, then… ok, but I'd test carefully on your slowest supported devices.
From the Instruments Time Profiler output, you can see that NSISEngine is eating up a ton of CPU. That class is responsible for doing the Auto Layout constraint evaluation and layout calculation.
So it looks like you are using Auto Layout, at least for some of the views.
Are you by any chance removing and re-adding constraints anywhere at runtime? I've seen this exact problem caused by that (can explain more if this is relevant).
If you aren't removing constraints, it sounds like you might have a fairly complex view hierarchy, and if you're using Auto Layout throughout, it's likely that you have a lot of constraints. As you may know, Auto Layout degrades in performance pretty quickly above a certain point due to super-linear time complexity of solving constraints. Check the output of po [[UIWindow keyWindow] recursiveDescription] from the debugger to see what your view hierarchy looks like.
I'm not sure what your view controller transition looks like, but you could try removing the disappearing view controller's view from its superview before doing the present. That should prevent it from doing layout calculations as it transitions. If that solves the performance issue, you could quickly snapshot the view hierarchy and then replace it with a single new UIImageView of the snapshot to display during the transition animation.
(One final thing: are any of your table views using Auto Layout in their cells? do any of these table views have more than ~20 cells?)
When presenting another controller your original controller has to be animated out of the window, which causes the view's frame to change and probably triggers all layoutSubviews methods and your manual adjustments.
You could try to avoid this by deactivating autoresizesSubviews in viewWillDisappear.
It is not very clean, but then probably all the calculations you are making may not be as well!
Try to optimize them:
Don't call layoutSubviews directly and call setNeedsLayout only if really needed.
Try to replace your manual resizing code with autoresizingMask or autoLayout.
Adjust views lazily and only if they are visible and if their size and not origin really has changed.
Don't reload your tables entirely but try to change only individual rows.
Try to use only a single table view.
Make sure to reuse cells.
The problem may have something to do with that you are placing UITableView instances inside a UIScrollView. That is explicitly prohibited by Apple's documentation for UIWebView (surprise!):
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behaviour can result because touch events for the two objects can be mixed up and wrongly handled.
I suspect that may also mess up table view cell reuse mechanism. Anyway, I'd also recommend to check if you are not 'leaking' any views at all. Keep in mind that even invisible views participate in layout if they are in the view hierarchy.
Edit: in response to Update 2
It's evident that cell reuse mechanism is not functioning properly. Try to make sure you are using correct instances of UITableView when dequeueing table view cells from reuse queue (check your data sources).

IOS: choice a UIScrollView or a UITableView

In my app I should create a view with a loto of informations;
these informations are divided in 4 section, every section can contain text, images lists ecc...
this is a brutal example...
Now I'm dubious to what type of solution to adopt.
In a my fast opinion a big scrollview is difficult to organize. And a big tableview with section is complicated to organize with code... What are your ideas?
UITableView is optimized for "reusable" cells, which is appropriate for scrolling in long lists.
Another benefit of using an UITableView, as others suggested, is that it only instantiate visible cells, so memory consumption is reduced.
In your case, since your content looks specific and non repetitive, I would suggest using a simple UIScrollView which is easier to use. (UITableView inherits from UIScrollView btw)
If memory/performance is an issue, then prefer UITableView or simply write your own logic to only instantiate views that are visible (by using scrollOffset for example)
EDIT:
On second thoughts, in your case, UICollectionView is surely a better candidate than UITableView.
Especially if you plan some day to do something like a 2 columns layout on iPad...
You should go with UITABLEVIEW, easy to manage easy to understand, more reusability and good memory management
If you have lots of content to scroll through, a UITableView might help you with keeping memory usage down.
When a cell scrolls out of sight, it gets removed from the view and kept around by the UITableView for later use (via -dequeueReusableCellWithIdentifier:). If you run low on memory then I believe that those invisible views (UITableViewCells) will get released. This basically means that your app will only keep views in memory that are actually visible. More will be cached, but can be purged any time if needed.
If you display a lot of data, and just add it all to a UIScrollView it will potentially use much more memory than if you used a UITableView. You might have to implement a similar mechanism to what UITableView does to remove (and potentially release) invisible views.
So, you can basically achieve the same effect, but a UITableView does a lot of that work for you already.
If you display lots of data (probably more than about two screens full) I'd lean towards using a UITableView.
This sort of thing is very easy to create in Interface Builder now with static cells; you can layout the entire interface visually and set up outlets for the cells (and/or their subviews) in order to configure the content in your view controller.

Too many UIViews? What's the cost, what are the tradeoffs?

EDIT:
Imagine that I'm in the business of reimplementing CoreText.
I'd get an NSString here which is an ordered list of code-points, and a set of glyphs there (an NSFont/CTFont) that basically is a list of CGPathRef.
Something in-between is in charge of the layout.
And some kind of environment must provide a CGGraphicContext to render into.
The kind of objects I have to display are CGPath. I am worried of creating one UIView for each CGPath! For a block of text, you wouldn't create an UIView for each character-glyph, would you?
I have a potentially large set of (tiny) objects to draw at once. In the 1000's. Many will be offscreen until scrolled into view.
One one hand it looks like my best bet would be to create one UIView for each one. The reasons are that:
with proper tiling I can make sure that the views that are offscreen
are not even in memory most of the time and brought up to life when
required
more importantly, when zooming I want to set each such view's layer contentScale to the zoomFactor such that I still get the full
precision and not the blurry stair-case
On the other hand I have no idea how the (otherwise perfectly generic) parent view will behave with 1000's of children.
So I am tempted to rethink my code such that the "children" just become an area of the single view.
But then I start thinking of setting that view's layer contentScale and I'm worried about exhausting memory just while zooming: I do not know if the layer machinery is "clever" enough not to create a huge backing bitmap when most of its contents would be invisible anyway
What to do, oh what to do?
Anyone has any experience they want to share?
I recently saw a presentation on a game involving hundreds of sprites implemented as UIViews, mostly image views. Performance was excellent on both an older and recent generation iPad. His code is available at https://github.com/bentford/ButtonWars/ , so you may want to test it to see if the performance meets your needs.
There is often an assumption that views are heavyweight, but experience doesn't bear that out.
If most of the objects will not be on the screen at any time, it's probably better not to create views for all of those off-screen objects. You need to intelligently add views for just those objects that are visible, and remove them from the view hierarchy when they go off-screen. You might be able to use UICollectionView to make this easy - it is very well designed, efficient, flexible, and easy to use and customize - but I don't know if it works with zooming.
If you decide to draw everything in one view, with deep zooming, you should look at CATiledLayer. It is designed for zooming in very deeply. Apple's MKMapView uses it under the covers to support scaling its viewport from Earth-sized down to a (relatively) few square meters.
If you cant use UICollectionView due to iOS version boundations, and you are intrested in re using views in UIScrollView, than have a look at this class. It is used the same way UITableview is used. Just pass in number of views and it will reuse the views.You can go through readme. Its very straightforward.

Tricks for improving iPhone UITableView scrolling performance?

I have a uitableview that loads fairly large images in each cell and the cell heights vary depending on the size of the image. Scrolling performance is decent, but can sometimes be jerky.
I found these tips I found on the FieryRobot blog:
glassy-scrolling-with-uitableview
more-glassy-scrolling-with-uitableview
Does anyone have any tips for improving uitableview scrolling performance?
Cache the height of the rows (the table view can request this frequently)
Create a least-recently-used cache for the images used in the table (and invalidate all the inactive entries when you receive a memory warning)
Draw everything in the UITableViewCell's drawRect: if possible avoid subviews at all costs (or if you require the standard accessibility functionality, the content view's drawRect:)
Make your UITableViewCell's layer opaque (same goes for the content view if you have one)
Use the reusableCellIdentifier functionality as recommended by the UITableView examples/documentation
Avoid gradients/complicated graphical effects that aren't pre-baked into UIImages
If you are subclassing
UITableViewCell, don't use a Nib,
write it in code instead. It's much
faster than loading Nib files.
If you're using images, make sure
you're caching them so you don't
have to load from file more than
once for each (if you have the
memory -- you'd be surprised how
much space images take up).
Make as many elements opaque as
possible. Similarly, try not and use
images with transparency.
The developer behind Tweetie has written extensively about this and has some code that demonstrates how it was done for that app. Basically, he/she advocates one custom view per table cell, and drawing it manually (rather than subviewing with Interface Builder, among other options).
fast-scrolling-in-tweetie-with-uitableview
Also, Apple has updated its own sample code for TableView in its TableViewSuite tutorials (maybe in response to this?)
TableViewSuite
#1 performance killer for UITableView scrolling is drawing shadows on any cell view layer, so if scrolling performance matters then don't do shadows unless basically it doesn't slow down your main thread.
thought this had to be said since none of the accepted answers made mention of shadows and layers. :+)
Any problem with UITableView scrolling performance can be solved using techniques already described in other answers. However many a times sluggish performance is caused by something inherently erroneous, or repetitive.
The fact that UITableView reuses the cells, and the fact that each cell may need its own image - together makes the solution bit complex. From how it's being solved the general way, here I summarize things that should be taken care of:
Load data into data source - from REST / database. This step should be done on background, eventually using dispatch_async along with GCD queue.
Create and initialize relevant data model objects and putting them inside an array
[tableView reloaddata]
Inside cellForRowAtIndexPath, include code that will set data (text) from correct data model object of the array.
Now images maybe in the form of URL too, so this step might be little quirky because of cell reuse done by table view. The heart of the fact is to load once again image from device cache / URL using async queue, then set it to correct cell.image (whatever is your cell image property).
To avoid problems, refer to this tutorial about lazy loading of images inside table view.

Resources