How to sort by multiple properties in Swift

⋅ 9 min read ⋅ Sort Array

Table of Contents

Sorting is easy if you do it over one criteria or a single property. Swift already has a function for that.

Here is an example where we sort an array of int.

let numbers = [3, 5, 6, 1, 8, 2]

let sortedNumbers = numbers.sorted { (lhs, rhs) in
return lhs < rhs
}

// [1, 2, 3, 5, 6, 8]

But there would be a time where you need to sort things over multiple criteria or properties. To demonstrate this, let's create a struct as an example.

Here we have a simple BlogPost struct with a title of the post and two statistic data points, page view, and session duration.

struct BlogPost {
let title: String
let pageView: Int
let sessionDuration: Double
}

And here is a sample data.

extension BlogPost {
static var examples: [BlogPost] = [
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
]
}

If you want to see which posts perform well, you might start by sorting them by page view. But as you can see, many posts aren't that popular and have the same page views. In this case, you need another criteria or property to do a further sort.

This kind of multiple properties sort is what we are going to talk about in this article. They are a variety of ways to tackle this problem. I will show the most basic approach without any advanced concept. You can make it as advance as you want once you understand the basics.

What is multiple criteria and properties sorting

Multiple criteria sorting mean a sorting in which we compare the first criteria, and only if the first criteria is equals, we then go to the next one. We do this until we find a non-equals criterion.

Pseudocode will look something like this:

let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] { // <1>
if lhsCriteria == rhsCriteria { // <2>
continue
}

return lhsCriteria < rhsCriteria // <3>
}
}

<1> We loop through the list of criteria, starting from the most important one (The first one).
<2> If the order criteria are equals and we can't decide the order, we move to the next criteria.
<3> If we can decide the order between two objects from the criteria, we stop and return the result.

If you have a hard time understanding the pseudocode, don't worry. I'm not a pseudocode professional writer. The following example should make it clearer.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Sort an array of objects over two fields

We will use the same scenario mentioned above. We want to sort BlogPost based on performances. Our performance decides by the number of page view (pageView), and if blog posts have the same page view, we use session duration (sessionDuration).

Here is a BlogPost struct and sample data that we use in the previous example.

struct BlogPost {
let title: String
let pageView: Int
let sessionDuration: Double
}

extension BlogPost {
static var examples: [BlogPost] = [
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
]
}

The way we measure performance can be translate into this code.

let popularPosts = BlogPost.examples.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView { // <1>
return lhs.sessionDuration > rhs.sessionDuration
}

return lhs.pageView > rhs.pageView // <2>
}

<1> If blog posts have the same page view, we use session duration.
<2> If the number of page views is not equal, we can decide an order by page view. (We sort by descending order)

Here is our result.

[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0), 
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]

Sort an array of objects over three fields

As you can see, it is very easy to perform a sort by two criteria. Let's increase more criteria into an equation. If blog posts have the same performance, we will sort them by name.

Let's add more blog posts to our examples.

extension BlogPost {
static var examples2: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
}

There is no different between two and three criteria. We can use the same logic as before.

let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView {
if lhs.sessionDuration == rhs.sessionDuration { // <1>
return lhs.title < rhs.title
}

return lhs.sessionDuration > rhs.sessionDuration
}

return lhs.pageView > rhs.pageView
}

<1> We add another if to check if blog posts have the same session duration and sort them by title if they got the same number of page views and session duration.

Results:

[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]

The problem

We can use the same logic for two and three criteria. The only problem here is that the more criteria, the more nested if-else you will need.

Here is an example of multiple criterias, which might lead to pyramid of doom.

let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
if lhs.pageView == rhs.pageView {
if lhs.sessionDuration == rhs.sessionDuration {
if lhs.nextCriteria == rhs.nextCriteria {
if lhs.nextCriteria == rhs.nextCriteria {
....
}

...
}

...
}

return lhs.sessionDuration > rhs.sessionDuration
}

return lhs.pageView > rhs.pageView
}

Sort an array of objects over N fields

To solve the pyramid of doom, let's bring back the pseudocode that we saw earlier.

let sortedObjects = objects.sorted { (lhs, rhs) in
for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
if lhsCriteria == rhsCriteria {
continue
}

return lhsCriteria < rhsCriteria
}
}

The code above is not the only way for this kind of problem, but the key should be similar. The key is we pack criteria into a collection where we can loop over.

extension BlogPost {
static var examples2: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]
}

typealias AreInIncreasingOrder = (BlogPost, BlogPost) -> Bool // <1>

let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [ // <2>
{ $0.pageView > $1.pageView },
{ $0.sessionDuration > $1.sessionDuration},
{ $0.title < $1.title }
]

for predicate in predicates { // <3>
if !predicate(lhs, rhs) && !predicate(rhs, lhs) { // <4>
continue // <5>
}

return predicate(lhs, rhs) // <5>
}

return false
}

<1> I declare an alias AreInIncreasingOrder which match sorting closure. This improves the readability when we declare our predicate collection.
<2> We declare a collection of predicates.
<3> We loop over predicates.
<4> Here is a tricky part, we want to check whether criteria can determine blog posts order or not. But AreInIncreasingOrder returns a boolean. How do we check if the order is the same? Let's examine AreInIncreasingOrder definition before answer that question.

AreInIncreasingOrder is a predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. So, two arguments are in equals order only if both arguments are not in increasing order.

That's mean our predicate must be false regardless of argument order. In other word lhs.pageView < rhs.pageView and rhs.pageView < lhs.pageView must equals false to be considered equals order. And that is what our !predicate(lhs, rhs) && !predicate(rhs, lhs) mean.

<5> If the order is equals, we continue to the next predicate.
<6> If the order is not equals, we can use that predicate to decides order.

Results:

[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0), 
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2.0),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0),
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Conclusion

Recently, I just came across this question and found it interesting. It is a simple task that takes me some time to grasp.

The methods in this article aren't tied to Swift. You can apply this to any language of your choice. You can improve the code to make it more generic that support any objects or properties you want, and I leave that as your exercise. If you come up with anything interesting, you can share your result with me on Twitter. I would love to see your implementation.


Read more article about Sort, Array, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
List view, a UITableView equivalent in SwiftUI

Part 3 in the series "Building Lists and Navigation in SwiftUI". We will explore a List, UITableView equivalent in SwiftUI.

Next
How to resize an UIImageView to fit a container view using auto layout

Learn how to fit image view to any container by changing Content Hugging Priority and Content Resistance Priority.

← Home