Good algorithm for reloading a batch of UICollectionView or UITableView changes? - ios

Setup: I have a UICollectionView or UITableView that’s backed by a simple array data source. I keep a copy of that data source in the controller.
Now, I get a notification from the system that there’s new data available. I get a new array where items may have been added, removed and changed positions.
So now I have two data objects:
previous array that's in sync with what the UI is currently showing
new array where items have been added, removed, moved
To get the UI in sync with the new array, I need to generate a bunch of UI calls. In case of UICollectionView, those are
- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths
And there’s a similar set of methods for UITableView.
I specifically don’t want to reload the whole table because that’s more expensive than just working with a few items.
So, the question is: given the previous and new data source array, how do I generate the correct set of UI calls, and when do I "swap out" my old data source for the new?

I'd see this as largely equivalent to the diff / patch problem, where the aim is to find the minimal number of changes between one text file and another and then to apply those changes. In that case, the implementation defines the operations:
add or insert
delete
change
... but not move. The reason for omitting move isn't immediately obvious to me but I'd strongly suspect that including move would require a very costly computation to find the optimal movement.
So, if we restrict the operations to those listed above, the Hunt-McIlroy algorithm described in An Algorithm for Differential File Comparison, or one of its descendents, would find a close-to-optimal set of changes.
The difference between your problem and the classic diff / patch is that you have a two-dimensional table, while diff / patch deals with a one-dimensional set of items (lines of text). The best way to convert the 2-D problem into the 1-D problem would depend on the particular characteristics of the changes that tend to be made in your data table.
For example, if the table is n rows by m columns and changes tend to be grouped in rows or rows are inserted or deleted as a whole, then you would likely be best to consider the table as if it was a text file and do the diff line by line. Alternatively, if changes tend to be grouped in columns or columns are inserted or deleted, you could do the diff column by column. If the alterations include inserting or deleting individual cells (which result in the subsequent cells shifting right or left as a result), you could treat the table as if each cell in the table was on a separate line of the text file, linearising the table in either row-first or column-first order.
Without knowing the details of your problem in this respect, however, my inclination would be to avoid premature optimisation. Thus, I'd tend to start by implementing the Hunt-McIlroy algorithm row-by-row if m < n, or column-by-column if n < m, then profile the application in use for a while before deciding that either a more sophisticated version of the algorithm or an alternative mapping of your problem to the Hunt-McIlroy solution was warranted.
A good discussion of a variety of diff algorithms can be found here on stackoverflow.

I have worked on an app that deals with a very similar problem that i was able to solve. I was expecting a complicated solution but it was really simple. Here is how you can solve it:
1) Create a new array in which you will receive new items and call it let us say 'moreItems', also synthesise it of course.
#property (nonatomic, readonly) NSMutableArray *moreItems;
2) In viewDidLoad of the ViewController that is linked with your TableView or CollectionView, alloc/init this array:
moreItems = [[NSMutableArray alloc] init];
3) Now you need to add your existing array let us say it was "items" to the new array that you have just created named 'moreItems'. You can do this using 'addObjectsFromArray'
[moreItems addObjectsFromArray:[channel items]];
'channel items' contains the objects it has received earlier and it will add these items to the newly created array called 'moreItems'. I am assuming you are collecting data from some web service, so you can implement this step in connectionDidFinsihLoading.
4) Now change your data source of TableView/CollectionView and replace it with the new array that is 'moreItems'
5) Next issue is that you don't want to reload the whole tableview and want to deal with new items. To have this functionality, you need to persist items. You can use archiving or core data whatever you are comfortable with.
6) The items that are already fetched, you will have to persist them let us say with archiving. And show them in the tableview when the user opens the app while it grabs more items that are updated on the web service.So first show the persisted items immediately and then handle the new items.
7) You need to look for some unique object, in my case it was the 'link' as every item has a different link and i can sort and handle them on this basis. You also need to use
- (BOOL) isEqual:(id)object
to compare the links on the web service and the links that are already in the tableview. This step is necessary because then your app won't add the items with links that are already in the tableview.
8) If there is some date associated with each item, you can use that date to sort them and show the new ones on top by using 'sortUsingComparator'
9) Also you need to use "[[self tableView] insertRowsAtIndexPath:rows withRowAnimation:UITableViewRowAnimationTop" to have that effect that shows user the new items have been added on top of the earlier ones.
Hope this helps.

I specifically don’t want to reload the whole table because that’s
more expensive than just working with a few items.
Are you sure this is true? Unless you have pathological case where every single cell is visible on your screen and there are hundreds of them, I find it hard to believe how will the "efficient" way be any faster than the "brute force" way.
Calling reloadData will only ask your delegate to update those cells that will end up visible on screen, it will not cause the entire table view to recreate every single of its cells.
If you're implementing some methods that change the complexity of calculating dimensions (like tableView:heightForRowAtIndexPath) you won't get any benefit using "efficient" way since the table view will have to recalculate all its heights anyway.
I'm not sure if solutions you are looking for will fix the problem, but let's just hope that I'm terribly wrong.

If your data source already has diff information, UITableView reloadRowsAtIndexPaths:withRowAnimation: will make it less-performance intensive:

Related

Can UITableViewDiffableDataSource detect an item changed?

(The question was rewritten after discussing with #AndreasOetjen below. Thanks for his comments.)
I ran into an issue with using UITableView with diffable data source. In my app when user modifies an item, it may change another item which is shown in the same table view. The issue is that, after I created and applied a new snapshot containing both items' new values, the indirectly changed item's UI wasn't updated.
At first I thought diffable data source was able to detect an item's value change in different snapshot. For example, it might work this way: if it found both snapshots contains the same item (that is, items in both snapshots have same hash value), it compared their values and updated that item's row in table view if value changed. However, I later realized it perhaps didn't work that way because diffable data source doesn't define any API to get and compare item value (my original thought was it used description computed property and == operation, but now I believe it's not true).
So my current understanding is diffable data source uses item's hash for detecting item order change (i.e., new item inserted, an old item still existed, etc.), instead of item value change (i.e., an old item still existed but its value changed). If that understanding is correct, it then begs this question: how to use diffable data source to implement the following scenario?
An item has several properties. One property (let's call it property A) is shown in UI but is not used for generating hash.
The item exists in both old and new snapshots, but its property A changes. So its UI needs to be updated.
In the old UITableView API, this can be implemented by calling reloadRows() or reloadData(). But how to do it using diffable data source?
UPDATE:
After spending time doing experiments and solving the issue, I believe the understanding in above question was incorrect. Please see my answer below. I believe that explains how diffable data source works. I hope it helps to others who'll have the same confusion. I'd be glad to be proved wrong. Really. So please leave your answer if you think differently.
After almost one day's clueless experiments, I believe I figured out how diffable data source worked and solved my issue based on that understanding (it turned out my original thought was almost correct).
Diffable data source uses item hash to identify item. For the same item that exists in both old and new snapshots, diffable data source checks if the item changes by doing an "==" operation with its old and new values.
Once figured out, it looks like quite obvious and simple approach. But it's so fundamental that I can't understand why it isn't mentioned explicitly anywhere.
So, to answer my original question, yes, diffable data source can detect item value change. That said, it becomes tricky when item value is of reference type and/or the text shown in row is, say, properties of objects referenced by that object (e.g., relationship in Core Data), etc.
Another note. Whether using entire item struct or just part of it to generate item hash doesn't matter, as long as it identifies the item. I prefer to using only the essential part of the item which really identifies it.
I'm a little confused about your last sentence: You write my item is an enum with associated values of reference type, but in your example above you use struct Book, which is a value type. Regardless of that, the following has to be kept in mind for any case:
Hashing is all about "object" identity. It's just a kind of shortcut to improve identity comparisons, folding etc.
If you provide a custom hash implementation, two objects a and b must behave in a way that a == b implies that also hash(a) == hash(b) (The other way round is almost always also true, but there may be collisions - esp. with weak hash algorithms - when this is not the case).
So if you only hash the title and author, then you have to implement the comparison operator in a way that it also only compares title and author. Then, if notes change, neither the data source nor any body will not detect identity changes at all.
UITableViewDiffableDataSource is a means to facilitate the synchronization of insert/delete statements between view and data source. If you ever got this
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 2 deleted).'
then a diffable data source is your friend.
I hope this helps a little.
I have the same problem. And after some research, I think Hashable is not the way to handle the updating feature. You can see it from the document here: https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources.
It has 2 ways to load the diffable data source: Load the Diffable Data Source with Identifiers and Populate Snapshots with Lightweight Data Structures .
While the first one is recommended by Apple. In which, we use snapshot.reconfigureItems to update the existing items.
struct Recipe: Identifiable, Codable {
var id: Int
var title: String
// and many other properties
xxxxx
}
// Get the diffable data source's current snapshot.
var snapshot = recipeListDataSource.snapshot()
// Update the recipe's data displayed in the collection view.
snapshot.reconfigureItems([recipeId])
recipeListDataSource.apply(snapshot, animatingDifferences: true).
The point is instead of using Recipe in the snapshot, we're using Recipe.ID, the type is NSDiffableDataSourceSnapshot<RecipeListSection, Recipe.ID>.
For the second way, which we're all using, putting the Hashable models in the snapshot, here is what Apple says about it:
The downside of this approach is that the diffable data source can no longer track identity. Any time an existing item changes, the diffable data source sees the change as a delete of the old item and an insert of a new item. As a result, the collection view loses important state tied to the item. For instance, a selected item becomes unselected when any property of the item changes because, from the diffable data source’s perspective, the app deleted the item and added a new one to take its place.
Also, if animatingDifferences is true when applying the snapshot, every change requires the process of animating out the old cell and animating in a new cell, which can be detrimental to performance and cause loss of UI state, including animations, within the cell.
Additionally, this strategy precludes using the reconfigureItems(:) or reloadItems(:) methods when populating a snapshot with data structures, because those methods require the use of proper identifiers. The only mechanism to update the data for existing items is to apply a new snapshot containing the new data structures, which causes the diffable data source to perform a delete and an insert for each changed item.
Storing data structures directly into diffable data sources and snapshots isn’t a robust solution for many real-world use cases because the data source loses the ability to track identity. Only use this approach for simple use cases in which items don’t change, like the sidebar items in this sample, or when the identity of an item isn’t important. For all other use cases, or when in doubt as to which approach to use, populate diffable data sources and snapshots with proper identifiers.

Advice - How to model detail list

I am working with a single TableViewController
The object that is displayed in TableViewController is Task
struct Task{
var type: String
var children = [Task]()
}
I store the base Task objects in a TaskStore. The TaskStore has a member pivot, which I use to track the current element in TaskStore. By Default, pivot is -1, and the TableViewController lists all objects in TaskStore. When a user clicks on a row in the TableViewController, I update pivot with the selected row, and display the Task objects in TaskStore.sharedInstance.get(pivot).children. The back button sets pivot back to -1 and displays the base TaskStore.
I am worried that this is overly complex. It works without a real problem, but I do need to add an exception whenever Pivot is not -1.
Is there a more suitable way to display the children of a Task object in TaskStore without the need to create a new TableViewController?
Thanks for any advice
The reason you are unhappy is that you are doing a lot of complicated work in your cellForRowAtIndexPath to pick up the right data. It has to interpret the pivot and dive appropriately into the task store. I suggest you forget all that. Knowing how to dive into the task store is not the business of the table view's data source!
What I would do, therefore, is separate the overall hierarchical model from the table's actual data source. The data source, at any given moment, should just consist of an array of Tasks, a [Task].
The problem then devolves into maintaining that array. When the user taps a row, you can replace that array of Tasks with whatever you like and call reloadData. In this way you maintain a direct correspondence between the table rows and tasks array. cellForRowAtIndexPath just asks for the info from that row of the Tasks array and it's done. Simple and clear.
Meanwhile, the work of knowing about your hierarchy all happens in the TaskStore, which is not, itself, the data source for the table. You might also need some sort of pointer into the TaskStore saying what the currently displayed parent is, but again, that bookkeeping will happen outside the array and outside the knowledge of your cellForRowAtIndexPath.
You thus end up with encapsulation of functionality. The task store understands its internal hierarchy and how to fetch a desired set of tasks. The table view knows how to grab its rows using a simple one-to-one correspondence with the current tasks list.

ios UITableView with fetchedresultscontroller - add custom rows

I have a UITableView with data coming from NSFetchedResultsController.
Here is my tablewView:
I need to add a row "All types". It also needs to be:
Sortable with all other items
Selectable (Design is now selected)
Selecting "All types" should deselect other rows
Give something to understand that it's an "All types" row when selected
I've read Add extra row to a UITableView managed by NSFetchedResultsController and NSFetchedResultsController prepend a row or section. Given approaches makes impossible to sort data or will look so hacky and produce so much hard-maintailable code, that it will be impossible to change logic and maintain code.
Are there any other good options?
PS. I understand, that my question may sound "broad" and doesn't containt code, but I think it's very common problem.
I do not think this is a very common problem at all. I can see it seems natural to do what you are trying but lets analyse your situation: What you generally have are 2 arrays of objects which you wish to sort as a single array. Now that is quite a common situation and I believe everyone knows how to solve this issue. You need to create a single array of objects and then sort it.
The way I see it you have 3 options:
Fetch all the items, merge the 2 arrays, sort and present them. This is not a very good idea since your memory consumption can be a bit too large if there are a lot of items in the database.
Put the extra data into the database and use a fetch result controller as you would normally. This should work good but you will probably need to mark these items so they are later removed or keep it in the database but ignore them where you wish not to display them.
Create a temporary database combined with what needs to be fetched from the database and your additional data. This approach is great if your data are meant for read-only in this list (which actually seems to be the case in what you posted). Still it is best if you create some kind of link between the objects. For instance some kind of ID would be great, this way when user selects an object from the second database you simply read the ID and fetch the object from the original database.

Can I (or Should I) use NSFetchedResultsController for One Table Section?

I have one section of a table which will display an initial set of comments, and then as new comments are entered and merged into core data, it will display those as well. I could simply tack all the new comments onto the end of the current array using a standard fetch, but I would also like to handle any potential comments that were added in the meantime, and may be mixed into the sort order of the current comments.
I was trying to decide how easiest to do all of this with NSMutableOrderedSet, but really nsfrc already does everything I need, except it works with multiple sections.
Is it wise to try and shoe-horn that in there?
If you are using Core Data, NSFetchedResultsController should probably be used for all your table views which display core data entities. Even those which are not updating in the background.
In this case you do have things being merged in the background, so NSFetchedResultsController is the logical and correct choice.

Insert extra row into UITableView like Tweetbot iOS

I have NSFetchedResultsController like datasource of my UITableView. It displays some entities with predicate from my database. I try to find an elegant solution to insert utility row between my data rows. I don't want to create fake entity in my database cause I don't want to mix View and Model. But I need to have ability to recreate this utility row (e.g. on other application launch). Any suggestions?
It should look something like this:
You're best bet, in my opinion, is to use a section header or footer for that "utility" row. In the case of Tweetbot, they're most likely caching results locally and then merge in data when the plus button is tapped. Your table will take multiple data sets as arrays (an array of arrays) and treat each separate array as a chunk and put it into its own section.
Any way you implement you'll want to wrap your results from the database with some sort of metadata. I think you're going to have to get away from a fetched results controller, unless you use a separate instance for each chunk, keeping track of the date range for each chunk.

Resources