Swift Sorting API

Doldamul·2023년 8월 3일
0
post-thumbnail
post-custom-banner

정렬은 배열을 비롯해 다양한 프레임워크 - 특히 Core Data 및 SwiftData - 에서 활용되는 기능이다. 혹시 SortDescriptor 등을 써보려고 정렬 API를 둘러보려다 당황한 적 있는지? 뭔가 이것저것 많은데, 각각이 뭔지는 감이 잘 잡히지 않는다. Cocoa 시절부터 사용해왔던 API, Swift로 새롭게 작성된 API, Obj-C 런타임과 호환되는 Swift API 등이 섞여있기 때문이다. 각 타입들을 단계적으로 살펴보며 사용방법을 간단히 알아보자.

❗️ minimum deployment가 iOS 14 이하인 경우, SortComparatorSortDescriptor를 사용할 수 없으므로 Comparator typealiasNSSortDescriptor를 사용해야만 한다. 다음 노션 페이지를 참조하자.

SortComparator

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

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

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.StandardComparator

String.ComparatorString 전용 정렬 옵션을 제공하는 comparator이며, String.StandardComparatorString에서 자주 쓰이는 정렬 옵션들을 타입 프로퍼티로 제공하는 comparator다.

let strings = ["abc", "안녕", "!", "2"]
let comparator = String.StandardComparator(.localizedStandard, order: .forward)
print(strings.sorted(using: comparator))
// ["!", "2", "안녕", "abc"]

SortDescriptor

마지막으로 SortDescriptor는 Cocoa 프레임워크 때부터 사용되던 NSSortDescriptor대체하기 위해 Swift로 재작성된 타입이다. Core Data, SwiftData를 비롯해 수많은 Obj-C 기반 프레임워크들에서 NSSortDescriptor를 사용하지만, NSSortDescriptorkeypath를 문자열로 명시해야 하고, 내부에 타입 정보를 보관하지 않아 동적 타입 검사를 사용하는 등의 단점이 있었다. iOS 15에 추가된 SortDescriptorkeypath를 문자열로 작성하는 대신 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]

SortDescriptorNSSortDescriptor는 서로의 생성자를 사용해 타입 변환이 가능하며, 따라서 서로의 사용처에 교차 사용될 수 있다. 단, 앞서 언급했듯이 정렬 대상 객체는 Obj-C 런타임에서 사용 가능해야만 한다는 제약이 있다.

// Core Data에서...
let fetchRequest = NSFetchRequest()
let sortDescriptor = SortDescriptor(\MyModel.name)

fetchRequest.sortDescriptors.append(.init(sortDescriptor))
// NSFetchRequest.sortDescriptors 프로퍼티의 타입은 [NSSortDescriptor]이다.

참고자료

Sorting API의 자세한 사용법은 공식 문서 또는 다음 노션 페이지들을 참조하자.

profile
덕질은 삶의 활력소다. 내가 애플을 좋아하는 이유. 재밌거덩
post-custom-banner

0개의 댓글