Writing a multi-attribute sort function using KeyPaths
Back in the days of Objective-C, there was a class called
NSSortDescriptor that would allow to declaratively write a sorting criterion, and would later be fed to a generic sorting function.
This system had the great advantage of easily allowing to perform a multi-attribute sort. The drawback lied in the absence of type safety: the API was stringly typed, as the name of the attributes would be provided in the form of an
The risk of crashing at runtime was thus unavoidable. For this reason, the use of
NSSortDescriptor is not very popular in the Swift community, as it clashes with the goal of statically checked types. Moreover,
NSSortDescriptor only works with Objective-C compatible types and attributes.
With Swift 4 a new construct called
KeyPath was introduced. It basically acts as a "pointer" to an attribute:
To obtain a KeyPath, we need to use the syntax
\Type.attribute. As with static members, the name of the type can be ommited when the context allows it to be inferred. It also possible to chain attributes (e.g.
The actual value can then be obtained with a syntax similar to that of a dictionary look-up:
With this mechanism, it’s now possible to take a “pointer” to any attribute of any type, wether it’s a class or a struct, and regardless of Objective-C compatibility.
Consequently, it’s also possible to leverage it in order to implement a type-safe multi-attribute sorting function
Writing the sorting function
We will declare our sorting method in an extension to
Array, and inside we'll delegate all the sorting logic to the standard
Array.sorted(by:). This way, our function will only have to translate the KeyPaths it will take as inputs into the right series of comparisons.
All the function does is to go trough the list of attributes until it finds one for which the value are not equal, then it uses it to perform the comparison.
Now we can generate a set of data to test the function:
And we can indeed observe that multi-attribute sorting is performed as expected:
For the purpose of this article, I voluntarily first kept things simple in order to make them easy to read and understand. Of course, in a real world situation some additions would be required.
For instance, our function only performs sorts in ascending order, which is very limiting. Nevertheless, it would be rather easy to provide tuples that contain both a KeyPath and a sorting order, and then write a new version of the sorting function that would use this new data in order to decide wether comparison must be ascending or descending.
This is not hard to implement:
And we can now perform more complex sorts, albeit at the price of a slightly more convoluted syntax:
Originally published at gist.github.com.