iOS 15 Sorting Evolution: Exploring SortComparator, SortDescriptor, and KeyPathComparator
Table of Contents
iOS 15 Sorting Evolution: Exploring SortComparator, SortDescriptor, and KeyPathComparator
Introduction
iOS developers have long relied on comparison closures and the sorted(by:)
method for data sorting. While effective, these approaches can become verbose or complex, especially when handling multi-level sorting or dynamic criteria. iOS 15 introduced three new sorting capabilities to the Foundation framework: SortComparator
, SortDescriptor
, and KeyPathComparator
.
These additions enhance sorting operations by providing more intuitive and flexible solutions for common sorting challenges. With iOS 18 approaching, iOS 15 features are now accessible to nearly all developers, making understanding and implementing these sorting methods increasingly relevant for both maintaining existing projects and optimizing new ones.
In this article, we’ll examine SortComparator
, SortDescriptor
, and KeyPathComparator
, highlighting their strengths and appropriate use cases. By exploring how these additions can improve sorting in our apps, you’ll gain the knowledge to choose the right sorting tool for your specific needs, potentially enhancing both app performance and developer productivity.
Understanding the “New” Sorting Trio
iOS 15 introduced three interconnected sorting components to the Foundation framework: SortComparator
, SortDescriptor
, and KeyPathComparator
. These additions offer more declarative and composable approaches to sorting compared to traditional closure-based methods.
SortComparator
SortComparator
is a protocol that defines a standard interface for comparison logic. It serves as the foundation for the other two components and can be used to create custom sorting implementations.
KeyPathComparator and SortDescriptor
Both KeyPathComparator
and SortDescriptor
are concrete implementations of SortComparator
. They share similar functionality but have some key differences:
KeyPathComparator
:
- Sorts based on key paths of properties
- Allows for custom comparators
- Lightweight and performs well in single-threaded environments
- Simple API for basic sorting needs
Example usage:
let nameComparator = KeyPathComparator(\Person.name)
people.sort(using: nameComparator)
SortDescriptor
:
- Also sorts based on key paths
- Offers a slightly different API
- Provides
Codable
andSendable
conformance - Suitable for complex sorting scenarios and cross-thread operations
Example usage:
let ageDescriptor = SortDescriptor(\Person.age, order: .reverse)
people.sort(using: ageDescriptor)
Both KeyPathComparator
and SortDescriptor
allow specifying ascending (.forward
) or descending (.reverse
) order and can be chained for multi-level sorting:
let sortDescriptors = [
SortDescriptor(\Person.lastName),
SortDescriptor(\Person.firstName),
SortDescriptor(\Person.age, order: .reverse)
]
people.sort(using: sortDescriptors)
Performance Comparison
To better understand the performance implications of these new sorting methods, I conducted a series of tests. Here’s the testing code I used:
import Foundation
import XCTest
struct TestItem {
let id: Int
let group: Int
let name: String
let value: Int
let date: Date
}
class SortingPerformanceTests: XCTestCase {
var testItems: [TestItem] = []
let itemCount = 100000
override func setUpWithError() throws {
try super.setUpWithError()
testItems = generateTestItems(count: itemCount)
}
func testKeyPathComparatorPerformance() {
let comparator = KeyPathComparator(\TestItem.value)
measure {
_ = testItems.sorted(using: comparator)
}
}
func testSortDescriptorPerformance() {
let descriptor = SortDescriptor(\TestItem.value)
measure {
_ = testItems.sorted(using: descriptor)
}
}
func testComplexKeyPathComparatorPerformance() {
let comparators: [KeyPathComparator<TestItem>] = [
KeyPathComparator(\TestItem.value),
KeyPathComparator(\TestItem.name),
KeyPathComparator(\TestItem.date)
]
measure {
_ = testItems.sorted(using: comparators)
}
}
func testComplexSortDescriptorPerformance() {
let descriptors: [SortDescriptor<TestItem>] = [
SortDescriptor(\TestItem.value),
SortDescriptor(\TestItem.name),
SortDescriptor(\TestItem.date)
]
measure {
_ = testItems.sorted(using: descriptors)
}
}
func testBaselinePerformance() {
measure {
_ = testItems.sorted { $0.value < $1.value }
}
}
func testComplexBaselinePerformance() {
measure {
_ = testItems.sorted { lhs, rhs in
if lhs.value != rhs.value {
return lhs.value < rhs.value
}
if lhs.name != rhs.name {
return lhs.name < rhs.name
}
return lhs.date < rhs.date
}
}
}
private func generateTestItems(count: Int) -> [TestItem] {
(0..<count).map { i in
let group = i / 100
return TestItem(
id: i,
group: group,
name: "Group \(group)",
value: group % 10,
date: Date().addingTimeInterval(Double(i))
)
}.shuffled()
}
}
Here are the results of our performance tests:
testKeyPathComparatorPerformance - Time: 0.391 sec
testSortDescriptorPerformance - Time: 0.586 sec
testComplexKeyPathComparatorPerformance - Time: 1.960 sec
testComplexSortDescriptorPerformance - Time: 2.245 sec
testBaselinePerformance - Time: 0.325 sec
testComplexBaselinePerformance - Time: 0.340 sec
Analysis of Test Results
Based on these results, we can draw several important conclusions:
Baseline Performance: Traditional, closure-based sorting methods remain the fastest for simple sorting tasks. This is evident from the
testBaselinePerformance
result of 0.325 seconds.Simple Sorting Comparison: For simple sorting tasks,
KeyPathComparator
(0.391 sec) outperformsSortDescriptor
(0.586 sec). However, both are slower than the baseline method.Complex Sorting: In complex sorting scenarios involving multiple criteria, both
KeyPathComparator
(1.960 sec) andSortDescriptor
(2.245 sec) show significant performance overhead compared to the complex baseline (0.340 sec).KeyPathComparator vs SortDescriptor:
KeyPathComparator
consistently performs better thanSortDescriptor
in both simple and complex scenarios. The performance gap is more pronounced in complex sorting tasks.Overhead of New Methods: While the new sorting methods introduce some performance overhead, they offer greater flexibility and more declarative syntax, which can lead to more maintainable code.
Scalability: The new methods exhibit a more pronounced performance difference between simple and complex sorting compared to the baseline. This suggests that the efficiency of these new methods may decrease more rapidly as sorting complexity increases.
Comparators in Action
To illustrate iOS 15’s sorting capabilities, let’s examine a practical SwiftUI example. This demo showcases the implementation of KeyPathComparator
, SortDescriptor
, and chained SortDescriptor
s in a near-real-world scenario.
In this example, we’ll create a simple app that displays a list of mythical creatures and allows the user to sort them using different criteria. This will demonstrate how easily these new sorting methods integrate with SwiftUI and how they can enhance user interaction in your apps.
struct MythicalCreature: Identifiable {
let id = UUID()
let name: String
let age: Int
let magicPower: Int
let habitat: String
}
@State private var sortMethod: SortMethod = .keyPathComparator
enum SortMethod {
case keyPathComparator
case sortDescriptor
case chainedSortDescriptors
}
private func sortCreatures() {
switch sortMethod {
case .keyPathComparator:
let comparator = KeyPathComparator(\MythicalCreature.name)
creatures.sort(using: comparator)
case .sortDescriptor:
let descriptor = SortDescriptor(\MythicalCreature.magicPower, order: .reverse)
creatures.sort(using: descriptor)
case .chainedSortDescriptors:
let ageDescriptor = SortDescriptor(\MythicalCreature.age, order: .reverse)
let habitatDescriptor = SortDescriptor(\MythicalCreature.habitat)
creatures.sort(using: [ageDescriptor, habitatDescriptor])
}
}
You can find the full code here.
This code creates a SwiftUI view displaying a list of mythical creatures. The user can sort the creatures using three different methods:
- By name using
KeyPathComparator
- By magic power using
SortDescriptor
- By age and then habitat using chained
SortDescriptor
s
The sorting method is selected through a menu in the navigation bar, and the list updates immediately to reflect the new sorting order.
Conclusion
The introduction of SortComparator
, SortDescriptor
, and KeyPathComparator
in iOS 15 marks a significant evolution in Swift sorting capabilities. While our performance tests show that these new methods don’t outperform traditional sorting in raw speed, they offer several advantages that make them valuable additions to an iOS developer’s toolkit:
Cleaner, More Declarative Code: The new methods allow for more readable and self-explanatory sorting logic, especially for complex sorting scenarios.
Improved Reusability:
SortDescriptor
andKeyPathComparator
can be easily combined and reused across different parts of an application.Type Safety: These methods leverage Swift’s type system, providing better compile-time checks and reducing the likelihood of runtime errors.
Flexibility: They offer a more flexible approach to defining sort orders, especially useful for dynamic sorting requirements.
Integration with Swift Ecosystem:
SortDescriptor
’sCodable
andSendable
conformance makes it well-suited for use in various Swift language features and frameworks.
When choosing a sorting method for your iOS application, consider the following guidelines:
- For simple, performance-critical sorting tasks, stick with traditional closure-based sorting.
- Use
KeyPathComparator
when sorting by a single property and performance is a concern. - Opt for
SortDescriptor
for complex, multi-level sorting or when you needCodable
/Sendable
conformance. - Implement a custom
SortComparator
when you need full control over the comparison logic.
Remember, the best tool for the job depends on your specific needs. While the new sorting methods introduce some performance overhead, their benefits in code clarity, maintainability, and flexibility often outweigh the slight performance cost, especially in non-performance-critical sections of your app.
As we move towards iOS 18 and beyond, mastering these sorting capabilities will become increasingly important. They complement existing sorting methods, providing developers with a broader range of tools to create efficient, maintainable, and flexible sorting solutions in their iOS applications.