정렬은 배열을 비롯해 다양한 프레임워크 - 특히 Core Data 및 SwiftData - 에서 활용되는 기능이다. 혹시 SortDescriptor
등을 써보려고 정렬 API를 둘러보려다 당황한 적 있는지? 뭔가 이것저것 많은데, 각각이 뭔지는 감이 잘 잡히지 않는다. Cocoa 시절부터 사용해왔던 API, Swift로 새롭게 작성된 API, Obj-C 런타임과 호환되는 Swift API 등이 섞여있기 때문이다. 각 타입들을 단계적으로 살펴보며 사용방법을 간단히 알아보자.
❗️ minimum deployment가 iOS 14 이하인 경우,
SortComparator
및SortDescriptor
를 사용할 수 없으므로Comparator
typealias 및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+)KeyPathComparator
String.Comparator
String.StandardComparator
SortDescriptor
ComparableComparator
는 비교 대상 타입이 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의 자세한 사용법은 공식 문서 또는 다음 노션 페이지들을 참조하자.