
정렬은 배열을 비롯해 다양한 프레임워크 - 특히 Core Data 및 SwiftData - 에서 활용되는 기능이다. 혹시 SortDescriptor 등을 써보려고 정렬 API를 둘러보려다 당황한 적 있는지? 뭔가 이것저것 많은데, 각각이 뭔지는 감이 잘 잡히지 않는다. Cocoa 시절부터 사용해왔던 API, Swift로 새롭게 작성된 API, Obj-C 런타임과 호환되는 Swift API 등이 섞여있기 때문이다. 각 타입들을 단계적으로 살펴보며 사용방법을 간단히 알아보자.
❗️ minimum deployment가 iOS 14 이하인 경우,
SortComparator및SortDescriptor를 사용할 수 없으므로Comparatortypealias 및NSSortDescriptor를 사용해야만 한다. 다음 노션 페이지를 참조하자.
SortComparator부터 시작해보자. iOS 15에 추가된 SortComparator 프로토콜은 컬렉션의 항목들을 정렬할 때 사용할 비교 방식을 나타내는 프로토콜이다. 간단히 말해, 정렬 알고리즘을 나타내는 프로토콜이다.
protocol SortComparator<Compared> : Hashable {
associatedtype Compared
var order: SortOrder
func compare(_ lhs: Self.Compared, _ rhs: Self.Compared) -> ComparisonResult
}
해당 프로토콜에서 사용되는 두 타입을 다음과 같이 이해할 수 있다:
SortOrder : 오름차순/내림차순 결정.ComparisonResult : 말 그대로 비교 결과를 나타낸 값. <(-1), ==(0), >(1)SortComparator의 사용 예시로, Sequence 프로토콜에서는 해당 프로토콜을 준수하는 타입을 인자로 받는 정렬 메소드를 제공한다.
extension Sequence {
func sorted<Comparator>(using comparator: Comparator) -> [Self.Element]
where Comparator : SortComparator,
Self.Element == Comparator.Compared { ... }
}
SortComparator 프로토콜을 준수하는 타입은 다음과 같다. 모두 구조체다. (모두 iOS 15+)
ComparableComparator (실질적으로는 iOS 17+)KeyPathComparatorString.ComparatorString.StandardComparatorSortDescriptorComparableComparator는 비교 대상 타입이 Comparable 프로토콜을 준수해야 하는 타입 제약이 걸려있으며, 비교 연산자를 통해 인스턴스를 비교한다. 사용법이 가장 간단하지만, 놀랍게도 생성자가 iOS 17+부터 사용 가능하다.
// iOS 17+
let arr = [4, 1, 3, 2, 5]
let comparator = ComparableComparator<Int>()
// order 인자는 생략 가능하며, 기본값으로 오름차순이 설정된다.
print(arr.sorted(using: comparator)) // [1, 2, 3, 4, 5]
KeyPathComparator는 비교할 타입에서 비교 대상 프로퍼티를 KeyPath로 명시하고, 다른 Comparator에 해당 프로퍼티의 비교 동작을 위임한다.
struct PersonInfo {
var name: String
var phoneNumber: String?
init(_ name: String) {
self.name = name
}
}
let arr: [PersonInfo] = [.init("John"), .init("Brandon"), .init("Charlie")]
let lexicalComparator = String.StandardComparator.lexical // 잠시후 설명한다.
let personComparator = KeyPathComparator(\PersonInfo.name, comparator: lexicalComparator)
print(arr.sorted(using: personComparator).map{$0.name}) // ["Brandon", "Charlie", "John"]
생성자에서 comparator 인자를 지정하지 않을 경우 내부적으로 ComparableComparator를 사용하기 때문에, iOS 15-16에서 ComparableComparator의 대안으로 사용될 수 있다.
let arr = [4, 1, 3, 2, 5]
let comparator = KeyPathComparator(\Int.self)
print(arr.sorted(using: comparator)) // [1, 2, 3, 4, 5]
String.Comparator는 String 전용 정렬 옵션을 제공하는 comparator이며, String.StandardComparator는 String에서 자주 쓰이는 정렬 옵션들을 타입 프로퍼티로 제공하는 comparator다.
let strings = ["abc", "안녕", "!", "2"]
let comparator = String.StandardComparator(.localizedStandard, order: .forward)
print(strings.sorted(using: comparator))
// ["!", "2", "안녕", "abc"]
마지막으로 SortDescriptor는 Cocoa 프레임워크 때부터 사용되던 NSSortDescriptor를 대체하기 위해 Swift로 재작성된 타입이다. Core Data, SwiftData를 비롯해 수많은 Obj-C 기반 프레임워크들에서 NSSortDescriptor를 사용하지만, NSSortDescriptor는 keypath를 문자열로 명시해야 하고, 내부에 타입 정보를 보관하지 않아 동적 타입 검사를 사용하는 등의 단점이 있었다. iOS 15에 추가된 SortDescriptor는 keypath를 문자열로 작성하는 대신 KeyPath 리터럴로서 명시할 수 있으며, 내부에 타입 정보를 보관하여 정적 타입 검사를 사용하고, SortComparator, Codable, Sendable 프로토콜을 준수한다.
SortDescriptor의 생성 방법은 comparator 인자를 받지 않은 KeyPathComparator와 사실상 동일하다. SortDescriptor의 경우 iOS 17+ 부터는 Obj-C 런타임에서 사용할 수 없는 타입도 정렬 대상으로 사용할 수 있지만, 이 경우 NSSortDescriptor로의 변환은 불가능하다.
// iOS 17+
let arr = [4, 1, 3, 2, 5]
let sortDescriptor = SortDescriptor(\Int.self, order: .reverse)
print(arr.sorted(using: sortDescriptor)) // [5, 4, 3, 2, 1]
SortDescriptor와 NSSortDescriptor는 서로의 생성자를 사용해 타입 변환이 가능하며, 따라서 서로의 사용처에 교차 사용될 수 있다. 단, 앞서 언급했듯이 정렬 대상 객체는 Obj-C 런타임에서 사용 가능해야만 한다는 제약이 있다.
// Core Data에서...
let fetchRequest = NSFetchRequest()
let sortDescriptor = SortDescriptor(\MyModel.name)
fetchRequest.sortDescriptors.append(.init(sortDescriptor))
// NSFetchRequest.sortDescriptors 프로퍼티의 타입은 [NSSortDescriptor]이다.
Sorting API의 자세한 사용법은 공식 문서 또는 다음 노션 페이지들을 참조하자.