Apple introduced the SortDescriptor
type and the related SortComparator
protocol in iOS 15. They are Swift friendly versions of the existing NSSortDescriptor
and NSComparator
.
I first saw SortDescriptor
show up when configuring SwiftUI fetch requests. You might also come across SortComparator
when working with SwiftUI Table
views. Here’s my quick notes:
What’s A SortComparator?
Apple added the SortComparator
protocol in iOS 15 (and macOS 12). I assume as a Swift friendly version of the much older Comparator
(formerly NSComparator
) which was a typealias for an Objective-C block used for comparison operations like sorting.
A type that conforms to SortComparator
has to implement two requirements:
-
A sort order property
// .forward or .reverse var order: SortOrder { get set }
-
A comparison method that returns the relative order of two elements based on the sort order:
func compare(_ lhs: Self.Compared, _ rhs: Self.Compared) -> ComparisonResult
Note: Possible values for ComparisonResult
are .orderedAscending
, .orderedSame
, and .orderedDescending
.
The only time, so far, that I’ve needed to create a standalone SortComparator
was when I was using values types with a SwiftUI Table
. See custom sort comparators for an example.
SortDescriptor
The SortDescriptor
type is also new in iOS 15. It conforms to SortComparator
and performs a similar role to the existing NSSortDescriptor
.
The standard library provides sort descriptor initializers for many of the common types including Date
, UUID
, Bool
, Double
, Int
, String
, and optional versions of those types. They require that the object you want to compare inherits from NSObject
and that the properties are visible to the Objective-C runtime.
You provide a key path to the property you want to compare and an optional sort order. For example, to sort an array of countries by reverse population:
class Country: NSObject {
@objc var name: String
@objc var capital: String?
@objc var population: Double
init(name: String, capital: String?, population: Double) {
self.name = name
self.capital = capital
self.population = population
super.init()
}
}
let populationDescriptor = SortDescriptor(\Country.population,
order: .reverse)
countries.sort(using: populationDescriptor)
The Foundation
framework includes standard comparators for strings using .lexical
, .localized
or .localizedStandard
ordering:
let lexicalComparator = String.StandardComparator(.lexical)
lexicalComparator.compare("2", "10") // .orderedDescending
let standardComparator = String.StandardComparator(.localizedStandard)
standardComparator.compare("2", "10") // .orderedAscending
You typically use this with a sort descriptor:
let nameDescriptor = SortDescriptor(\Country.name,
comparator: .localizedStandard)
let sorted = countries.sorted(using: nameDescriptor)
Compare this to the equivalent NSSortDescriptor
version which uses strings for the keys and bridges back to NSArray
for sorting:
let sortDescriptor = NSSortDescriptor(key: "name",
ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))
let sorted = (countries as NSArray).sortedArray(using: [sortDescriptor])
You can, if needed, convert between the two types of sort descriptor:
// Create a NSSortDescriptor from a SortDescriptor
let nsDescriptor = NSSortDescriptor(nameDescriptor)