One of the earliest things you learn about Core Data is that it doesn’t support ordered entities out of the box. Sometimes you can have an ordered relationship, but the minute you try and move that database to iCloud, you find that ordered relationships aren’t supported. Long story short: if you want to order your entities, then you need to create an index attribute that allows you to sort your entities yourself.
So you do the natural thing: create your index attribute as an Int, and then assign each in order: 1, 2, 3…
This is fine if the data doesn’t change, but if this is data the user can add to and reorder, what if they want to move element 3 between 1 and 2? It means you need to change 2 entities in addition to adding the new one. What if there are 500 entities, and the user wants to insert a new first element. 500 entities to change because the user moved a single entity?!
There must be a way to insert and reorder IDs that avoids all of this rewriting, right?
Thankfully there are a couple of techniques you can use that will almost completely eliminate the need to rewrite a huge number of entities every time you need to insert or rearrange entities. Let’s talk about them now.
If you step back and think about the problem, the issue with creating entities in between new entities is that those entities already have adjacent IDs, like 1 and 2 in our example above, and there is no space in between. Then, a better solution presents itself: create that space by separating your IDs by more than 1. This gives you space to insert new entities or move entities in between existing entities.
How much space do you need between each ID? Any value is acceptable, but the larger you set the distance between two entities, the more you can insert before you need to think about reindexing.
Now that the space is there, how do you calculate the new ID for a new or moving entity? To do that, simply average the IDs of the two surrounding entities. So for our example above, if we chose a span of 32, we would have entities with IDs of 32, 64, and 96. Moving the third entity in between the first two would mean we would change the ID of the third entity from 96 to (32+64)/2=48. Our new list would be reordered, and the IDs would be 32, 48, and 64.
This solution gets you about 95% of the way there, but eventually, after your list has been edited and rearranged a lot, those gaps can still get split up enough to where eventually the same adjacent ID situation will occur. You might think that now would be the time to reindex your entire list, but if you do that while a user is doing things in your app, it could cause a sudden surprising — and frustrating — delay for the user.
There is a more efficient way to accomplish this without reindexing your entire list: you locally reindex just the entities that need to be updated in order to accomplish the insert or move.
So, let’s say the user is trying to insert a new entity in between two entities where the IDs are 32 and 33. Instead of reindexing the entire list, look at the next entity on either side of those entities. Those entities are still spaced out, those entities have IDs 0 and 64. Great, there’s some space there.
To insert the new entity, you set the ID for the new entity, and update the IDs of the two surrounding entities so that their IDs are evenly spaced in the gap. In this case, those two outer entities are separated by 64, so we set the new entity to ID 32, and the two entities it is going between to 16 and 48. This results in an in order list with IDs 0, 16, 32, 48, and 64. And we only had to touch 3 entities; if your list had had 100 entities, reindexing would have touched all 100 entities.
What if even when you expand to include those surrounding records, you still can’t evenly space the 3 records without a conflict? Then you broaden the span, taking the two records surrounding that group of 5. Repeat until you find a span which will allow the records to be inserted in between. In practice, though, this will rarely expand beyond the case shown above.
The result of all of this is a more responsive Core Data app with ordered entities: no lags, no surprises.