Sorting in Swift

Sorting in Swift

Sometimes you have a collection of elements that are not in the order that you need them. Fortunately, Swift gives us an easy way to get them from that chaotic state into an ordered state. In this article we're going to look at how to use the sort methods, how we can clean up the code around them, and how we can sort objects that aren't necessarily sortable as a whole.

Breaking It Down

If you've taken a computer science class, or are learning fundamentals, you have probably learned about insertion sort, quicksort or merge sort or one of the others and how to implement them. That's good stuff to know, but in practice we don't usually want to think at that level because we are likely to make mistakes in our code and there is almost always a way to optimize things further beyond what we know. So Swift has these two methods sort(by:) and sorted(by:). They provide an abstraction around what sorting method is used, so you don't have to care. They just ask you for a closure that they can call on any two elements and you have to return a boolean that lets the sort function know if those two elements are in increasing order.

mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]

Let's dig into that, but first I just want to note that the main difference between them is sort sorts your collection in place and sorted returns a new array with the elements sorted. I almost always use sorted, but there are times when performance can be improved significantly by using sort instead, so it is good to keep in the back of your head. For the rest of this article, I'm only going to refer to sorted.

Looking at the function signature might be a little intimidating. I know it was for me when I was first trying to read the documentation. I mean, what is rethrows? It turns out, the throws and rethrows in this context are things you can ignore until you need them. (If you can't stand not knowing, check out Paul Hudson's article on the topic.) So let's just pretend that this is the signature:

func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]

This is a function which you can call on a collection, it's only parameter is a closure that takes two Elements and returns a Bool, and the function itself returns an [Element]. The closure is where you tell the sorter how to sort. It might look something like this:

let unsortedArray = [3, 2, 5, 1, 4]

let goingUp = unsortedArray.sorted(by: { first, second -> Bool in
    return first < second
})
// goingUp == [1, 2, 3, 4, 5]

let goingDown = unsortedArray.sorted(by: { first, second -> Bool in
    return first > second
})
// goingDown == [5, 4, 3, 2, 1]

So whatever method is actually used for sorting, it will go through and any time it needs to make a decision of "should this element go first or that one?" it will call this closure and based on the answer it will know where each element needs to go. This is already pretty powerful, but it gets even better.

Same Thing, But Shorter

We can communicate the same thing in way fewer characters because of some nice syntactic sugar in Swift. Let's see all the steps we can take to reduce our code here.

First, we can make it a trailing closure and get rid of the argument label.

let goingUp = unsortedArray.sorted() { first, second -> Bool in
    return first < second
}

We can get rid of the return type definition, because the sorted method already has that as a part of its signature so it can be inferred here. If you try to write a closure here that doesn't return a boolean, the compiler will complain.

let goingUp = unsortedArray.sorted() { first, second in
    return first < second
}

Because this is one line, we can also get rid of the return keyword at the end of the closure. The compiler will assume that you intend to return the result of this line (as long as it evaluates to a boolean).

let goingUp = unsortedArray.sorted() { first, second in
    first < second
}

We also get some free parameter names that are based on the argument position in the closure, so we can get rid of the parameter names too. $0 will be the first argument, counting up from there.

let goingUp = unsortedArray.sorted() {
    $0 < $1
}

We can also get rid of the parentheses because there are no other arguments except this trailing closure. At the same time, this is so short we can even fit it on one line.

let goingUp = unsortedArray.sorted { $0 < $1 }

That is much shorter, and it puts the central logic right in front of you. We're sorting in the order where the smaller elements will come first. But don't feel bad if you get mixed up with such short statements, especially when it hasn't made it into your muscle memory yet. I think it is a good practice to start with the fully expanded version while you're working the logic out and then go through these steps to try to condense it from there. Over time, it will become second nature for you.

In this case, we can actually take it even farther. Because operators in Swift are basically just special syntax for functions, and functions are themselves closures, and because the signature for < matches the signature for the closure we're looking for in sorted, as long as the elements in our array are Comparable we can just pass the operator as the parameter.

let goingUp = unsortedArray.sorted(by: <)

Last but not least, if we want to sort in ascending order (which we do in this case) and our elements are Comparable, we don't actually have to provide any argument because that is the default.

let goingUp = unsortedArray.sorted()

Sorting On Properties

Another thing that makes sorted so powerful is that you don't have to sort on the whole element, you can sort on one of its properties instead. This is super helpful because it is not uncommon to have an object where the whole object is not Comparable, but some of its properties are. For example, think about how you would sort a group of people. Would you sort them by age? or by name? by height? Whatever you chose, you would almost certainly sort them by some specific attribute (or combination of attributes), not by them as whole people. It’s hard to even imagine what that would mean. Besides that, in some contexts it may make sense to sort them by name, but in others it may make more sense to sort them by interest, or height, or hair color. And the same is true in our code. Fortunately, Swift makes this really easy to do.

struct Person {
    let name: String
    let createdAt: Date = Date()
}
let wayne = Person(name: "Wayne")
let derry = Person(name: "Derry")
let dan = Person(name: "Dan")

let unsortedPeople = [dan, wayne, derry]

let alphabetical = unsortedPeople.sorted { $0.name < $1.name }
// alphabetical == [dan, derry, wayne]

let oldestFirst = unsortedPeople.sorted { $0.createdAt < $1.createdAt }
// oldestFirst == [wayne, derry, dan]

In the first example, we sort on the person's name and the second we sort on the date when that person was created. If your object has further nested properties, you could sort on those too! It just comes down to whatever makes the most sense in your use case. And that is the beauty of sorted. You don't have to worry about how to sort elements, you only have to think about what you want to sort them by.

For some extra sugar, you can even add a simple extension that I picked up from John Sundell that will let you sort on a keypath. I won't go into detail here about what keypaths are or how to use them, but check out John's article if you're interested. Here's what that looks like.

extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }
}

let alphabetical = unsortedPeople.sorted(by: \.name)
// alphabetical == [dan, derry, wayne]

A couple of things to note about this extension. First, this method assumes that you want to sort in ascending order, but you can customize it however makes sense for your use case. You could even further extend it to also take in a closure so that the sort order could be defined at the call site. But I'll leave that as an exercise for you to try out if you want.

Second, notice that the extension is on Sequence, not on Array. This is because of another very powerful feature of Swift, the collection protocols. Again, I'm not going to go into a ton of detail here, but check out Harshil Shah's article on Swift's Collection Types. It is about as close to comprehensive as I have come across, and he does a great job of making it understandable. What it means for us here is that this extension will add this sorted method to any type that conforms to Sequence. That means not only can we call it on an Array, we can also call it on a Set, a Dictionary, or even on a String(although I'm not sure how often that is useful).

let set = Set([wayne, derry, dan])
let alphabetical = set.sorted(by: \.name))
// alphabetical == [dan, derry, wayne]

let numberOfPuppers = [wayne: 6, derry: 4, dan: 9]
let byLeastDrunk = numberOfPuppers.sorted(by: \.value)
// byLeastDrunk == [(key: Derry, value: 4), (key: Wayne, value: 6), (key: Dan, value: 9)]

let sortedLetters = "Sushis and sashimis.".sorted(by: \.description)
// sortedLetters == [" ", " ", ".", "S", "a", "a", "d", "h", "h", "i", "i", "i", "m", "n", "s", "s", "s", "s", "s", "u"]

Wrap Up

So there you have it. We've looked at how to use sort/sorted even though their signatures might be a little intimidating. We've looked at how to use them with as few characters of code as possible. And we've looked at how to sort objects that may not make sense to sort as a whole, but have properties you can sort on. I hope that gives you a better picture of how to use these methods. If you have any questions, tips, tricks or stories about sorting in Swift, be sure to share them in the comments.

Did you find this article valuable?

Support Dillon McElhinney by becoming a sponsor. Any amount is appreciated!