Sort array of objects by multiple properties with Swift Tuple

⋅ 6 min read ⋅ Swift Sort Array Tuple

Table of Contents

In my last article, How to sort by multiple properties in Swift, we learn how to sort an array of objects with multiple criteria.

The final implementation can sum up like this.

A 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.

Which can translate into pseoducode like this:

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
}
}

It involves declaring an array of predicates and looping, but we can remove most of that logic with tuple.

Tuple comparison

Swift has overload comparison operators like <, >, <=, >= for us with the following implementation.

/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
///
/// - Parameters:
/// - lhs: A tuple of `Comparable` elements.
/// - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func < <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A : Comparable, B : Comparable

I want you to focus on the following sentence:

/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).

This is exactly how we implement our sorting. Let's compare that to our pseudocode. You can see that tuples comparison is how we compare our sorting criteria.

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
}
}

Let's implement the sorting using a tuple of criteria.

Example

We will use the same object as our last example.

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

Ascending order

To sort in ascending order, we use less than operator (<) over tuples.

let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, lhs.pageView, lhs.sessionDuration) < (rhs.title, rhs.pageView, rhs.sessionDuration)
}

print(sorted)

Here is the result:

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

This will compare the first element lhs.title < rhs.title and if lhs.title == rhs.title, we compare (lhs.pageView, lhs.sessionDuration) < (rhs.pageView, rhs.sessionDuration). This is identical to our previous implmentation.

Descending order

To sort in descending order, we use less than operator (<) over tuples.

let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, lhs.pageView, lhs.sessionDuration) > (rhs.title, rhs.pageView, rhs.sessionDuration)
}

print(sorted)

Here is the result:

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

Different sort order on each element

If you want different sorting orders for each property, you can do that by swap the element's position in the tuples.

We can't change the operator for each element in the tuples, but swapping its position would result in the same effect. Consider the following examples.

lhs.property1 < rhs.proerty1 will sort in ascending order.
lhs.property2 > rhs.proerty2 will sort in descending order, but all elements must compare using the same operator. So, we swap the position.
rhs.property2 < lhs.property2 is equals to lhs.property2 > rhs.property2 which is descending sort.

So, if we want to sort title in ascending order and sort pageView and sessionDuration in descending order, we can use the following code.

let posts: [BlogPost] = [
BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
return (lhs.title, rhs.pageView, rhs.sessionDuration) < (rhs.title, lhs.pageView, lhs.sessionDuration)
}

print(sorted)

Caveats

We can only compare up to six properties using tuples. Swift only overload tuple comparison up to six elements.

/// Returns a Boolean value indicating whether the first tuple is ordered
/// after or the same as the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is after or the same as the second tuple if and only if
/// `a1 > b1` or (`a1 == b1` and
/// `(a2, ..., aN) >= (b2, ..., bN)`).
///
/// - Parameters:
/// - lhs: A tuple of `Comparable` elements.
/// - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func >= <A, B, C, D, E, F>(lhs: (A, B, C, D, E, F), rhs: (A, B, C, D, E, F)) -> Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable

I would say six properties should be enough for most cases, but if it isn't, you can go back to my last implementation.


Read more article about Swift, Sort, Array, Tuple, 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
How to create segmented control in SwiftUI

Learn the way to create the UISegmentedControl equivalent in SwiftUI.

← Home