I'm trying to insert and delete a large amount of items (say, 20,000) from a collection view, and the operation takes a very long time.
The test fixture I created is composed of the following:
UICollectionView with no configuration besides a data source.
Default UICollectionViewFlowLayout.
Data source that returns either 10K or 30K items depending on a BOOL variable.
Button to toggle that variable. When set to YES, 20K items are being added to the data source (just by changing numberOfItemsInSection:) and insertItemsAtIndexPaths: with 20K items. When set to NO, deleteItemsAtIndexPaths: is called with 20K items.
Cell configuration in the data source does nothing besides dequeuing a default UICollectionViewCell and returning it.
Running this on simulator, which should be faster than any device, yields the following timings:
Insertion of 20K items: 220ms.
Deletion of the same 20K items: 1100ms.
This is, by all means, horribly slow, especially when performed on the main thread.
Here's a screenshot from instruments, showing the hotspots in UICollectionView's internal implementation (specifically, _computeItemUpdates):
I've noticed that the use of reloadData instead of inserting or updating the items is way faster (~20ms), probably because no animations are triggered so there's no need to compute the position of each item and section for animation purposes.
Any ideas on how to overcome this would be appreciated.
Expand _computeItemUpdates. If anything it's calling is yours, then yes you can.
An example would be if you are using a custom layout, you could ask it to calculate the new positions on a background thread and then call insert/delete when that operation finishes.
You could also be smart about it and only call insert/delete for ranges that are currently visible, then after the rearrange animation finishes you could reloadData and it shouldn't look too different from a users perspective.
Related
I'm implementing search with autofill. I have UITextField and UITableView. When user changes text in text field I'm loading new data from server and displaying it in table view.
Number of results is fixed(5 or less). Height of table is connected to number of results, so it will never scroll. When new results arrive i simply call reloadData() for table.
So the question is should i use dequeueReusableCell() or create new cells with UITableViewCell(), as they will never reuse? Will reloadData() clear all memory for cells if they were created without dequeuing?
The main purpose of dequeueReusableCell() is to reuse the UITableViewCell that is created already in memory, instead of recreating it, therefore, it runs faster on the CPU than having to recreate a UITableViewCell each time you need one. It has nothing to do with the content you give it.
So it's always preferable to use dequeueReusableCell() as it makes the app run smoother. Even if the difference is negligible.
As it's clear from the title, I have a scrolling performance issue in a table view.
Since I have read nearly every question that is posted online in this regard, and I assume all of you have much more experience with UITableView and its techniques, I won't bother with general stuff, and I just wanna point out some key things in my code that may help you help me spot where I'm doing wrong.
The UI in each cell is very very basic, so rendering each doesn't take considerable time. No shadows, no rounded corners, no extra effect, nothing. Just a few labels and two images, that's all.
The datasource is an NSArray which is already fetched from CoreData. The data of the labels are set from the content of the array, without much calculations or process required.
The height is each cell is a static integer, so the tableView:HeightForRowAtIndexPath: will immediately return the result as fast as possible. No calculations required.
The tableView:CellForRowAtIndexPath: dequeues and reuses cell with reusable identifiers so any re-creation is avoided.
So far everything is perfectly smooth. The issue is where items in Core Data are fetched from a server (Which is extremely fast) as user scrolls down. Data binding is done inside tableView:willDisplayCell:atIndexPath: to prevent tableView:CellForRowAtIndexPath: from becoming slow, as data needs to be loaded just before the cell goes live on the screen. I also fetch new items from server inside this method whenever there're some cells remaining till the last item fetched. So for example when there are totally 50 cells data fetched and put in the CoreData already and this method is called for cell number let's say 40, I request another 50 cell data from server, so that it will be ready whenever user reaches the end of the table.
As I expect this should only be called for the cells that go live on the screen. But putting some NSLogs shows that it is called multiple times until next 200 cells data are fetched (I guess the amount changes depending on device or simulator and the memory available on them and also OS limits). For example, I'm testing on an iPhone 7+, and I start the app and I go the page in which the table is. It fetches first 50 items and only first 4 items are shown on the screen, But I see that tableView:willDisplayCell:atIndexPath: is also called for cell #25, so another 50 is fetched immediately, and then it is called for cell #75, so another 50 is fetched, and this goes on for like first 200-300 cells, and then when fetching is stopped, scrolling is extremely fast and optimized until next 200-300 cells are fetched.
What can I do? Shouldn't tableView:willDisplayCell:atIndexPath: fire whenever a cell is about to be displayed? Where else should I fetch data as user scrolls?
Any ideas or suggestions is REALLY and GREATLY appreciated.
As the user types, I am retrieving results (text) by traversing a local word graph in a background thread. Each keystroke cancels the previous operation. If the operation completes, the data source is updated and reloadData is called on the main thread. This works great and is very fast (as fast as the user can type), even when tens of thousands of results are returned.
To customize the size of each collection view cell, I implemented sizeForItemAtIndexPath for the UICollectionViewDelegateFlowLayout delegate. Unfortunately, this results in a small, but noticeable lag when the user types. To be sure the time was not lost in my size calculation logic, I tried just returning a fixed size, but it still killed performance. I am surprised at this because there are only ~120 cells or so on the screen at any given time. When commenting out this method, the response time is again immediate, even for very large data sets.
Any ideas for improving the performance of UICollectionView with custom cell sizes?
Thanks
Additional clarification...
The program returns all possible words from the given set of letters then sorts by score or alphabetically, etc. As the user types, the total word count goes up fast (exponentially, if multiple wildcards are entered). The words change as you type so the width of the cells update accordingly and wrap to the next line as handled by the flow layout.
The issue seems to be the number of cells shown on the screen at any given time. In sizeForItemAtIndexPath, if I just return a large size where only one or two cells are visible, the update is very fast; however, if I return a size that just fits the text, I end up with 100+ visible cells and there is a lag. If I comment out sizeForItemAtIndexPath and just use a fixed size cell, it is fast, but that is not what I am going for.
You don't need to reload UICollectionView by calling reloadData , instead you can use:
[self.collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
Here, indexPath is the NSIndexPath object for the corresponding UICollectionViewCell object
that you want to update.
I am using core data in my app to store entities that could have as many as 50k objects or more. I have this paired to an NSFetchedResultsController in a table view. The table view works fine due to cell reuse however my biggest problem is queuring the actual database to get the dataset.
When i first load the table view i need all results from the db. I am using the default fetch request with a single sort descriptor and I have set the batchSize to 1,000. On an iPad 2 this query takes up to 15 secs to finish! I also have to run this query after a search has been cancelled so overall it makes the app unusable. My assumption is that CD still has to resolve all those results or setup the sections or something, i really have no idea but just using the batchSize doesn't help?? The content is also very dynamic in the sense that new rows are always getting added, sort order changing etc.. so caching has a limited benefit.
I am thinking now that the best option would be to use a fetchLimit in the fetchRequest and then implement some basic paging. When the table view scrolls to the end fetch the next "page" of results? My only problem with this approach is that i lose the sectionIndex and i cant think of any way around that.
Anyone have any ideas or dealt with this issue already?
When you set the fetch request for the FRC the batch size should be just a few items bigger than, maybe twice the size as, the number of items that can be seen on screen at any one time. The FRC already does the pagination for you you just need to set the page size better.
s.newave,
Do your rows have variable height? If so, then the table view asks you to calculate each height and that causes every row to be fetched. 15 seconds is not an unreasonable time to fetch 50K items.
The bigger problem is your statement about not wanting to change your design. Frankly, a 50K item tableview is useless. You should change your design -- not because CD is slow, it isn't -- but because your design is not pragmatically usable.
Andrew
P.S. The fetched results controller is designed for mainstream applications. a 50K table view is not a mainstream app. If you insist on keeping with a 50K table view design, you will have to make your own controller.
I have a UITableView that collects data from a database. What I would like to know is if there is some way I can iterate in the UITableView collection and check the values of the cell? The reason I ask is because I would like to update each cell based on the current value that it has (change font, size, color, etc.). I've seen in another SO post regarding this topic, but since the cells are already created and their values are changed it is a bit harder for me. I was thinking of iterating through the UITableView before I call reloadData, but any other suggestions are welcome.
You should not iterate over the cells of UITableView, because some of them (in fact, most of them) may not be present until you request them. UITableView aggressively recycles its cells, so if a cell is not visible, it is very likely that you would be creating it from scratch only to put it back into recycle queue moments later.
Changing your model and calling reloadData the way your post suggests would be the right solution. iOS will ensure that it runs the update in a smallest number of CPU cycles possible, so you do not need to worry about the cells that are already created. This is also the easiest approach in terms of your coding effort.
A table view is for displaying data. The properties of your table cells should only be written to, not read from. The appropriate way of handling this situation would be to update your underlying model objects -- the objects that you use to populate the table view -- as the data changes, and then reload the affected rows.
The issue you'll encounter is that UITableView reuses table cells. Once a table cell scrolls off the screen, it's quite likely that the table view will reuse the same cell to display a different row.
This means it's fundamentally not possible to iterate over the table cells. When you need to refresh a row because its data has changed, you should call reloadRowsAtIndexPaths:withRowAnimation: (or reloadData if all rows have changed) and if the row is visible on screen, UITableView will call your data source methods and give you an opportunity to configure the cell for display.