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 as NSString
.
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.
With Swift 4 a new type called KeyPath
came along. It basically acts as a "pointer" to an attribute:
https://gist.github.com/5a0d95577a022ab955c937500c2479f8
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. \UIView.layer.cornerRadius
)
The actual value can then be obtained with a syntax similar to that of a dictionary look-up:
https://gist.github.com/2d2b36129780ad54644321e13826f84f
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
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 a set of comparisons.
https://gist.github.com/3b45fb34fa9e2fdd2112d3ac81abcfb7
All the function does it go trough the list of attributes until it finds one where the value are not equal, and use it to perform the comparison.
Now we can generate a set of data to test the function:
https://gist.github.com/bfe9f33b64e15d36517c9cf8cf415f9e
And we can indeed observe that sorting is performed as expected:
https://gist.github.com/5479a24d77e7ba449fcc4ce1d393e929
For the purpose of this article, I voluntarily 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 create a new type that provides 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:
https://gist.github.com/2ebd27b4d32df5ee7ef7608a27ecdebd
And we can now perform more complex sorts, albeit at the price of a slightly more convoluted syntax: