Convenient Sorting With Swift Key Paths

Posted 9/30/2018.

Key paths were introduced in Swift 4 and allow you to reference a type's properties without evaluating them. In this tutorial, we'll use key paths to write a generic array extension that makes sorting more convenient with code that is easier to read.

First, let's look at an example type, City, and review how we would use the built-in standard library methods to sort an array of cities.

struct City {

    let name: String
    let country: String
    let population: Int

}

let beijing = City(name: "Beijing", country: "China", population: 21_707_000)
let buenosAires = City(name: "Buenos Aires", country: "Argentina", population: 3_054_300)
let london = City(name: "London", country: "United Kingdom", population: 8_825_001)
let losAngeles = City(name: "Los Angeles", country: "United States", population: 3_976_322)
let mexicoCity = City(name: "Mexico City", country: "Mexico", population: 8_875_000)
let nairobi = City(name: "Nairobi", country: "Kenya", population: 3_138_369)
let newYork = City(name: "New York", country: "United States", population: 8_443_675)
let shanghai = City(name: "Shanghai", country: "China", population: 24_183_300)


let cities = [beijing, buenosAires, london, losAngeles, mexicoCity, nairobi, newYork, shanghai]

To get an array of city names sorted by population descending using the standard library, we would write:

let standardMostPopulousCities = cities.sorted(by: { $0.population > $1.population }).map { $0.name }
print(standardMostPopulousCities)

Not bad, but using key paths we can do better. The goal is to be able to write the following:

let mostPopulousCities = cities.sorted(by: \.population, isAscending: false).map(\.name)

We also want to be able to sort in the same way by name or country. Key paths are generic types and are specialized using the root type whose properties you are accessing and the type of the property itself. In the case of our City type the key path for name and country would be of type KeyPath<City, String> and the key path for population would be of type KeyPath<City, Int>.

In order to sort by any value, that value must be of a type conforming to Comparable. To make a type's key paths sortable then, we just need to make sure all properties are of Comparable conforming types. This is already true of City, as String and Int both conform to Comparable.

With this in mind, we can write the following extension on Array:

extension Array {

    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, isAscending: Bool = true) -> [Element] {
        return sorted {
            let lhs = $0[keyPath: keyPath]
            let rhs = $1[keyPath: keyPath]
            return isAscending ? lhs < rhs : lhs > rhs
        }
    }

}

Now we can sort by any property of City, in ascending or descending order. To make mapping to a key path slightly more convenient, we can also add the following to our Array extension:

func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
    return map { $0[keyPath: keyPath] }
}

Using the extension, we can now write any of the following:

let mostPopulousCities = cities.sorted(by: \.population, isAscending: false).map(\.name)
let reverseAlphabeticalCities = cities.sorted(by: \.name, isAscending: false).map(\.name)
let citiesGroupedByCountry = cities.sorted(by: \.country).map(\.name)

The first line returns the city names sorted by highest population. The second line returns the city names sorted in reverse alphabetical order. And the third line returns the city names sorted by country.


In this tutorial, we saw how we can use key paths to make sorting by a type's properties more convenient. The code is also more expressive and easier to understand. We can now say we want "cities sorted by population ascending" instead of passing in a sorting function.

The source code for this project is on GitHub here.