Swift code to efficiently store user-defined order
Hello,
If you enjoy reading my content and want to support my work, you can purchase the source code containing the solution I explain in this post along with some real world examples on how to use it (example app in SwiftUI with models defined as Observable objects/Core Data entities and SwiftData models).
If you have any questions or suggestions, feel free to reach out on X/Twitter!
Thanks,
Axel
In many apps I build, I one day need to allow the user to manually order a collection of elements (e.g. folders, groups, projects, tags, items, tasks, steps, instructions, songs, photos, you name it!). I wanted to do this in an efficient way to avoid as much as possible unnecessary database updates (including index/rank redistribution in case of collision) and network calls (I mainly use CloudKit with Core Data/SwiftData).
Here are some solutions that came to my mind along with the solution I chose.
Order elements using ordered relationships in Core Data
This is the first approach I used in my apps, mainly when data was only local with no server/CloudKit synchronisation or any distributed system. It was “easy” because I didn't have to think about managing the order of the elements myself, it was all managed for me by the Core Data framework.
Unfortunately, this solution didn't work as soon as I wanted to use Core Data + CloudKit (NSPersistentCloudKitContainer) because NSOrderedSet are not supported.
So I had to find another way to allow manual ordering. The starting point was to add a new attribute ‘rank’ (or any name you prefer like index, position, displayOrder, etc.) to my models so I could sort my entities myself.
Order elements using a numerical position with an increment of 1
I initially thought about storing this new rank attribute as Integer in my model. When I add an element to a collection, I assigned it a rank equal to the number of objects in the collection. It’s convenient because I can directly order the elements. However, I had to re-order each successive element in the database to modify the order: best case is when I have to change anywhere between one other element, and worst case is when I have to update all the elements. For instance, moving the 499th element to the top would result in 500 updates. And because my data is synchronised with a remote server, CloudKit will need to send the updated information to the server. And then the server needs to notify all clients about the modifications. This gets even worse if I make many updates in the app… This "naive" solution is quite inefficient and something more optimal should be used.
Order elements using a numerical position with an big increments
One way to reduce this unnecessary work is to use gaps between sequential elements in the collection to distribute the indexes sparsely: a gap of 100, 1000 or more for example. This is actually how NSOrderedSet is implemented in Core Data: to manage ordered relationships, Core Data calculates an object’s index in the UInt16 space as being exactly in the middle of the previous and next objects in line. When the integer space runs out (like what integer to use between 12 and 13?), Core Data steps an object out in either direction and redistribute the objects. When a user moves an element, I calculate the average position between the previous neighbour and the next one (moving an element between 0 and 100 would have the index 50 for example). This works fine, until you run out of space between two positions. In this situation, I would need to use a recover process to redistribute the ranks.
Order elements using floating numbers
I then thought about using a floating number (Double or Decimal) to have more space between two elements. With this approach, I get roundoff error, space limit, as well as performance impacts (as indexing and sorting in a database based on floats is more expensive that Integers). And I'll always wind up with collisions and the corresponding need to redistribute values. So this solution does not solve the problem encountered with integers, it will just take more time before I have to clean run the recover process.
Order elements using a Linked List like data structure
Another solution is to use Linked List in the database, where each element has a reference to the next element in the list (for example a relationship back to itself in Core Data/SwiftData, also called a reflexive relationship). This makes changing the order less expensive because only two elements need to be updated, but it requires the app to implement the linked-list traversal logic to derive the order. This would basically mean to load all elements in memory in order to extract the final sort order, as FetchRequest or Query can’t handle this natively unfortunately. For the frameworks I use to build my apps, this approach is not an appropriate solution.
Order elements using a String
After reading some StackOverflow questions on this topic, I discovered that Jira uses a ranking system called LexoRank, based on a lexicographical order where the main idea is to use a string. String are easy to sort, have arbitrary positions and allow a ton of breathing room in between ranked elements. And if you run out of room, you can append more characters to the string. With this approach, only the elements that is inserted/moved need to have its rank updated: this also means only one element to send to the remote server and other clients. And you don’t need to redistribute the ranks (at least, I’ve not encountered a case where it was needed).
My solution
I wrote a simplified version of this ranking system that can be used to easily store user-defined order. It’s perfect when drag and dropping elements in a SwiftUI List for example, powered by Core Data or SwiftData.
The attached project shows how to:
- Create: when I add a new element in the collection, I first find the rank used by the last element in the collection, then generate the rank for the element to be inserted.
- Update: when I move an element, I first find the previous and next ranks around the destination (drop) index, then generate the rank for the element to be moved and update its rank.
- Delete: when I delete an element, I just have to remove the element.
In these 3 operations, only one element in the collection needs to be updated.
The source code with real world examples on how to use it in Core Data/SwiftData or a more generic data model (Observable objects from Observation framework)