SortComparator and SortDescriptor

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:

  1. A sort order property

    // .forward or .reverse
    var order: SortOrder { get set }
    
  2. 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)