Sorting collections of custom types using higher order functions in Swift

While sorting simple types are covered well in Apple's documentation on the subject and in many other places, I've focused below on sorting custom object types

Sorting collections of custom types using higher order functions in Swift

While sorting simple types are covered well in Apple's documentation on the subject and in many other places, I've focused below on sorting custom object types

This post is a cheat sheet for sorting sequences of custom objects using Swift's sort operators.  While sorting simple types (String, Int) are covered well in Apple's documentation on the subject and in many other places, I've focused below on sorting custom object types, which is more relevant to my daily work.

The Object

I'll be sorting a collection of the following model object in this cheat sheet. Note the struct conforms to Comparable in order to enable default sorting behavior and to allow code to compare the order of two Book objects.

struct Book: Comparable {
    let title: String
    let price: Double
    let pageCount: Int
    static func < (lhs: Book, rhs: Book) -> Bool {
        return lhs.title < rhs.title
    }
}
The Book Object

The examples in this post will operate on the following collection of Book objects.

The Collection

let books:[Book] = [
    Book(title: "Don Quixote", price: 9.99, pageCount: 992),
    Book(title: "The Great Gatsby", price: 14.99, pageCount: 108),
    Book(title: "Moby Dick", price: 13.45, pageCount: 378),
    Book(title: "War and Peach", price: 10.89, pageCount: 1152),
    Book(title: "Hamlet", price: 11.61, pageCount: 244),
    Book(title: "The Odyssey", price: 14.99, pageCount: 206),
    Book(title: "The Abbot", price: 8.49, pageCount: 164),
    Book(title: "Catch-22", price: 20.06, pageCount: 544),
    Book(title: "To Kill a Mockingbird", price: 7.19, pageCount: 336),
    Book(title: "Gulliver's Travels", price: 13.99, pageCount: 312)
]
The Book Collection

Default Sort via Comparable Conformance

sorted() Returns a copy of a sequence of Comparable items as an Array.  Most base Swift types are Comparable. For a custom object like Book, adding conformance is required, but also trivial (see the Book declaration above).

To sort by the built-in sort order of a Type, just call the .sorted() function on the collection. Simple!

let sortedBooks = books.sorted()
Sort using Comparable conformance

Since the Book type has a built-in "<" operator that sorts by title, sorted() can be called without parameters to return an array in the following order.

Catch-22                  20.06  544   
Don Quixote               9.99   992   
Gulliver's Travels        13.99  312   
Hamlet                    11.61  244   
Moby Dick                 13.45  378   
The Abbot                 8.49   164   
The Great Gatsby          14.99  108   
The Odyssey               14.99  206   
To Kill a Mockingbird     7.19   336   
War and Peach             10.89  1152  
Sort using Comparable conformance result

Sort by Alternate Property

We often would want to sort in a different order than that provided by than the built-in Comparable conformance operator--or would like to sort a collection that has no Comparable conformance at all.

Use the following syntax to sort by page count in descending order:

let sortedByPageCount = books.sorted { (lhs, rhs) -> Bool in
    return lhs.pageCount > rhs.pageCount 
}
Sort by alternate property

This sort orders the collection as follows. Note the use of ">" in the closure inverted the sort to create an array sorted by page count in reverse numerical order.

War and Peach             10.89  1152  
Don Quixote               9.99   992   
Catch-22                  20.06  544   
Moby Dick                 13.45  378   
To Kill a Mockingbird     7.19   336   
Gulliver's Travels        13.99  312   
Hamlet                    11.61  244   
The Odyssey               14.99  206   
The Abbot                 8.49   164   
The Great Gatsby          14.99  108
Sort by alternate property result

Sort with Custom Property Calculation

In the first example (using the build-in Comparable conformance), the output was according to the "<" operator's result, which was to sort by the title property.  

This is accurate according to the English alphabet, however in English when a title starts with "The", we sort by the next word, as if the "The" didn't appear at the beginning of the title.

We'll fix this using two approaches. In the first, I'll implement the "The" rule by adding an extension to the String object that adds a function to strip a leading "The" word, and then sort by the return of that extension function.

Note that I only want to apply this rule to English, and have queries the system Locale to skip this process in any other language.

Here's the extension:

extension String {
    func theToEnd() -> String {
        let prefix = "The"
        guard Locale.current.languageCode?.starts(with: "en") == true,
              self.lowercased().hasPrefix(prefix.lowercased()) else 
              { return self }
        
        return String(self.dropFirst(prefix.count)
                   .trimmingCharacters(in: .whitespaces))
    }
}
String extension

And then using that function within the .Sort { ... } expression:

let sortedWithoutThe = books.sorted { (lhs, rhs) -> Bool in
    return lhs.title.theToEnd() < rhs.title.theToEnd()
}
Sort using String extension

This returns a new array with the following contents:

The Abbot                 8.49   164   
Catch-22                  20.06  544   
Don Quixote               9.99   992   
The Great Gatsby          14.99  108   
Gulliver's Travels        13.99  312   
Hamlet                    11.61  244   
Moby Dick                 13.45  378   
The Odyssey               14.99  206   
To Kill a Mockingbird     7.19   336   
War and Peach             10.89  1152  
Sort using String extension result

Bonus: Moving the custom Title sort into the Comparable operator

Since (in English) we would don't sort titles by the word "The", you might wonder, wouldn't it have been better to move this rule into the Book model's Comparable conformance?  Yes, it almost certainly would. So let's do it!

Move the titleWithoutThe() function from the String extension to the the Book model:

struct Book: Comparable {
    let title: String
    let price: Double
    let pageCount: Int
    static func < (lhs: Book, rhs: Book) -> Bool {
        return lhs.titleWithoutThe() < rhs.titleWithoutThe()
    }
    
    private func titleWithoutThe() -> String {
        let prefix = "The"
        guard Locale.current.languageCode?.starts(with: "en") == true,
              title.lowercased().hasPrefix(prefix.lowercased()) else 
              { return title }
        
        return String(title.dropFirst(prefix.count)
                           .trimmingCharacters(in: .whitespaces))
    }
}
Modify Comparable conformance with custom title rule

Since this logic only applies to Book titles, it makes more sense as part of the Model class, doesn't it?

Now we can use the original .sorted() function to achieve the same result while avoiding the String extension and customized .sort { } syntax.

let sortedBooks = books.sorted()
Custom title rule sorting

The resulting array will be ordered as follows:

The Abbot                 8.49   164   
Catch-22                  20.06  544   
Don Quixote               9.99   992   
The Great Gatsby          14.99  108   
Gulliver's Travels        13.99  312   
Hamlet                    11.61  244   
Moby Dick                 13.45  378   
The Odyssey               14.99  206   
To Kill a Mockingbird     7.19   336   
War and Peach             10.89  1152  
Custom title rule sorting result

Levels of Conciseness in Sort Closures

In the following code block, sorted1, sorted2, sorted3 and sorted4 all return the same output array, and illustrate how it's possible to remove syntax from sort closures.

// Original array (var to enable .sort() in place sorting)
var strArray = ["zebra", "antelope", "horse", "fox"]

print(strArray)

// Sort into new arrays
let sorted1 = strArray.sorted( by: { a, b in a < b} )
let sorted2 = strArray.sorted( by: { $0 < $1} )
let sorted3 = strArray.sorted( by: < )

print(sorted1)
print(sorted2)
print(sorted3)

// use a function for the logic
func customSortFunc(_ a: String, _ b: String) -> Bool {
    return a.lowercased() < b.lowercased()
}
let sorted4 = strArray.sorted( by: customSortFunc )

print(sorted4)

// Sort the original array in place
strArray.sort(by: <)
print(strArray) // original array is now sorted version

// Output:
["zebra", "antelope", "horse", "fox"]
["antelope", "fox", "horse", "zebra"]
["antelope", "fox", "horse", "zebra"]
["antelope", "fox", "horse", "zebra"]
["antelope", "fox", "horse", "zebra"]
["antelope", "fox", "horse", "zebra"]

Tags:

Rob Kerr

App development for iOS, creating applications for my own development studio (Cuvenx Inc.), and consulting with awesome clients to build their mobile applications.

More posts from this author