Sorting arrays is a common task in Swift. In this episode, you learn a handful of patterns to sort arrays, including sorting arrays of objects and sorting arrays by property. We use several functions that are defined in by Swift's Standard Library, such as sort
and sorted
. Are you ready?
The Basics
Let's start by creating a playground in Xcode. Add an import statement for the Foundation framework at the top.
import Foundation
We start simple with an array of integers based on the Fibonacci sequence. We store the array of Int
s in an constant with name numbers
.
import Foundation
var numbers = [2, 144, 3, 5, 89, 13, 21, 1, 34, 8, 1, 55, 0]
We have two options to sort the array in Swift, sorted()
and sort(). The
sorted()method returns an sorted copy of the array whereas
sort()mutates the array of numbers. So
sorted()returns a copy whereas
sort()` is a mutating method or function.
import Foundation
var numbers = [2, 144, 3, 5, 89, 13, 21, 1, 34, 8, 1, 55, 0]
let sortedNumbers = numbers.sorted() // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
numbers.sort() // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
This implies that the array you want to sort needs to be mutable (a variable) if you want to use the sort()
method. The compiler kindly reminds you if you forget this detail.
The Comparable Protocol
You may be wondering how the sorted()
and sort()
methods know how to sort the array of numbers. The type of the elements of the array need to conform to the Comparable
protocol. The elements are sorted in ascending order.
Many Foundation types conform to the Comparable
protocol, including the String
struct. In this example, we sort an array of coffee beverages.
import Foundation
var beverages = ["Espresso", "Cappuccino", "Latte", "Americano", "Mocha"]
let sortedBeverages = beverages.sorted() // ["Americano", "Cappuccino", "Espresso", "Latte", "Mocha"]
beverages.sort() // ["Americano", "Cappuccino", "Espresso", "Latte", "Mocha"]
Sort Array in Descending Order
To sort an array in descending order, you invoke sorted(by:)
method and pass in the sort order, the less-than operator (<) for ascending order and the greater-than operator (>) for descending order.
import Foundation
var numbers = [2, 144, 3, 5, 89, 13, 21, 1, 34, 8, 1, 55, 0]
let ascendingNumbers = numbers.sorted(by: <) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
let descendingNumbers = numbers.sorted(by: >) // [144, 89, 55, 34, 21, 13, 8, 5, 3, 2, 1, 1, 0]
Sorting an Array of Objects
Let's make it a bit more interesting and sort an array of objects. We first declare a simple struct we can work with.
import Foundation
struct Movie {
// MARK: - Properties
let title: String
let releaseDate: Int
}
We create an array of movies using the Movie
struct. I'm a fan of Harry Potter so ...
let movies = [
Movie(title: "Harry Potter and the Sorcerer's Stone", releaseDate: 2001),
Movie(title: "Harry Potter and the Chamber of Secrets", releaseDate: 2002),
Movie(title: "Harry Potter and the Prisoner of Azkaban", releaseDate: 2004),
Movie(title: "Harry Potter and the Goblet of Fire", releaseDate: 2005),
Movie(title: "Harry Potter and the Order of the Phoenix", releaseDate: 2007),
Movie(title: "Harry Potter and the Half-Blood Prince", releaseDate: 2009),
Movie(title: "Harry Potter and the Deathly Hallows – Part 1", releaseDate: 2010),
Movie(title: "Harry Potter and the Deathly Hallows – Part 2", releaseDate: 2011),
]
If we try to sort the array using the sorted()
and sort()
methods, we run into a compiler error. Can you guess why that is? As I wrote earlier, the type of the elements of the array needs to conform to the Comparable
protocol.
As an alternative, we can sort the array by passing a closure to the sorted(by:)
method. In the closure, we define how the elements of the array need to be sorted. In this example, we sort the movies by the Movie
struct's releaseDate
property in descending order.
movies.sorted(by: { movie1, movie2 in
movie1.releaseDate < movie2.releaseDate
})
We can shorten the implementation by using trailing closure syntax.
movies.sorted { movie1, movie2 in
movie1.releaseDate > movie2.releaseDate
}
We can shorten the implementation even more by using shorthand argument names.
movies.sorted { $0.releaseDate > $1.releaseDate }
If we declare movies
as a variable, we can also use the sort(by:)
method.
var movies = [
Movie(title: "Harry Potter and the Sorcerer's Stone", releaseDate: 2001),
Movie(title: "Harry Potter and the Chamber of Secrets", releaseDate: 2002),
Movie(title: "Harry Potter and the Prisoner of Azkaban", releaseDate: 2004),
Movie(title: "Harry Potter and the Goblet of Fire", releaseDate: 2005),
Movie(title: "Harry Potter and the Order of the Phoenix", releaseDate: 2007),
Movie(title: "Harry Potter and the Half-Blood Prince", releaseDate: 2009),
Movie(title: "Harry Potter and the Deathly Hallows – Part 1", releaseDate: 2010),
Movie(title: "Harry Potter and the Deathly Hallows – Part 2", releaseDate: 2011),
]
movies.sort { $0.releaseDate > $1.releaseDate }
Conforming to the Comparable Protocol
If you do a lot of sorting in your app, then it is much more convenient to conform the type your are sorting to the Comparable
protocol. This requires two steps. First, conform the Movie
struct to the Comparable
protocol. Second, implement the less-than operator (<) and the equal-to operator (==). We only need to implement the less-than operator. Swift implements the equal-to operator for us.
import Foundation
struct Movie: Comparable {
// MARK: - Comparable
static func < (lhs: Movie, rhs: Movie) -> Bool {
lhs.releaseDate > rhs.releaseDate
}
// MARK: - Properties
let title: String
let releaseDate: Int
}
With the Movie
struct conforming to the Comparable
protocol, we can shorten the implementation even more.
movies.sort()
Advanced: Using a SortComparator
Apple added one other option to sort collections a few years ago, the SortComparator
protocol. This protocol is available as of iOS 15.0, tvOS 15.0, macOS 12.0, and watchOS 8.0. It is a more advanced option, but it can be powerful and convenient. I like how a sort comparator encapsulates the nitty-gritty details of the sorting logic.
Declare a struct with name MovieSortComparator
that conforms to the SortComparator
protocol.
struct MovieSortComparator: SortComparator {
}
The SortComparator
protocol has a few requirements. We first defined a type alias for Compared
, the type of the elements of the collection we want to sort.
struct MovieSortComparator: SortComparator {
// MARK: - Type Aliases
typealias Compared = Movie
}
We also need to declare a stored property with name order
of type SortOrder
. As the name suggests, the order
property defines the sort order of the sorted collection after the sort comparator has done its work.
struct MovieSortComparator: SortComparator {
// MARK: - Type Aliases
typealias Compared = Movie
// MARK: - Properties
var order: SortOrder
}
The magic happens in the compare(_:_:)
method. It accepts two elements of the collection and its return type is ComparisonResult
. The possible values are orderedSame
, orderedAscending
, and orderedDescending
.
struct MovieSortComparator: SortComparator {
// MARK: - Type Aliases
typealias Compared = Movie
// MARK: - Properties
var order: SortOrder
// MARK: - Methods
func compare(_ lhs: Movie, _ rhs: Movie) -> ComparisonResult {
}
}
In the body of the compare(_:_:)
method, we use a guard
statement to return early if the release dates of both movies are the same. In that scenario, we return orderedSame
.
struct MovieSortComparator: SortComparator {
// MARK: - Type Aliases
typealias Compared = Movie
// MARK: - Properties
var order: SortOrder
// MARK: - Methods
func compare(_ lhs: Movie, _ rhs: Movie) -> ComparisonResult {
guard lhs.releaseDate != rhs.releaseDate else {
return .orderedSame
}
}
}
In the remainder of the implementation, we compare the release dates of the movies. Note that we take the value of the order
property into account to determine the return value.
struct MovieSortComparator: SortComparator {
// MARK: - Type Aliases
typealias Compared = Movie
// MARK: - Properties
var order: SortOrder
// MARK: - Methods
func compare(_ lhs: Movie, _ rhs: Movie) -> ComparisonResult {
guard lhs.releaseDate != rhs.releaseDate else {
return .orderedSame
}
if lhs.releaseDate > rhs.releaseDate {
if order == .forward {
return .orderedDescending
} else {
return .orderedAscending
}
} else {
if order == .forward {
return .orderedAscending
} else {
return .orderedDescending
}
}
}
}
Using the MovieSortComparator
struct is very easy. We create a MovieSortComparator
object, set its order
property through the initializer, and pass it to the sorted(using:)
method. That's it.
let sortComparator = MovieSortComparator(order: .forward)
movies.sorted(using: sortComparator)
The SortComparator
protocol is quite powerful and it makes it very easy to (1) centralize sort logic and (2) unit test sort logic. For example, you could add a property to MovieSortComparator
to define the property to use for the sorting operation. In the compare(_:_:)
method, you would then inspect the value of the key
property. This makes the sort comparator much more flexible.
struct MovieSortComparator: SortComparator {
// MARK: - Types
enum Key {
// MARK: - Cases
case title
case releaseDate
}
// MARK: - Type Aliases
typealias Compared = Movie
// MARK: - Properties
let key: Key
// MARK: -
var order: SortOrder
// MARK: - Methods
func compare(_ lhs: Movie, _ rhs: Movie) -> ComparisonResult {
switch key {
case .title:
// ...
case .releaseDate:
// ...
}
}
}
This is what it looks like at the call site. This looks pretty neat. Right?
let sortComparator = MovieSortComparator(
key: .title,
order: .forward
)
movies.sorted(using: sortComparator)
What's Next?
In most situations, you find yourself using the sorted(by:)
and sort(by:)
methods. The SortComparator
protocol is very useful, but it is a more advanced approach to sort collections. I encourage you to explore it if you find you are repeating yourself in various places of your codebase.