로컬 Swift 배열을 여러 필드 기준으로 정렬하려면 우선순위에 따라 각 키를 비교하고, 동률일 때만 다음 키로 넘어갑니다. 선택적 대체 값을 별도로 처리한 다음, 값이 없는 항목을 어디에 둘지 정하세요. 이 예제는 Mac 데스크톱 목록에 사용할 합성 도형 레이블을 정렬합니다.

클로저를 작성하기 전에 정렬 규칙을 정의하세요

데스크톱 목록에 그룹 번호, 선택적 기본 위치, 선택적 보조 위치, 제목이 있는 도형 레이블이 있다고 가정해 보겠습니다. 목표는 메모리에 있는 이 값들을 예측 가능한 하나의 표시 순서로 만드는 것입니다.

두 가지 결정에는 각각 별도의 규칙이 필요합니다. 대체 규칙은 각 항목을 나타낼 값을 고릅니다. 기본 위치가 있으면 그 값을 사용하고, 없으면 보조 위치를 사용합니다. 타이브레이커는 두 항목의 유효 위치가 같을 때만 다른 필드를 비교합니다. Swift의 nil 병합 연산자 ??는 래핑된 값이 있으면 이를 선택하고, 없으면 대체 값을 선택해 첫 번째 결정을 지원합니다. 기본 연산자 — nil 병합 연산자

이 예제에는 다음과 같은 명시적 규칙을 적용합니다.

우선순위 키 규칙
1 그룹 숫자가 작은 항목 우선
2 유효 위치 preferredPosition ?? backupPosition
3 위치가 없는 항목의 배치 각 그룹 내에서 위치가 있는 항목 뒤에 배치
4 제목 위치가 같으면 Swift 문자열 비교로 오름차순 정렬
5 식별자 앞선 모든 키가 같으면 식별자가 작은 항목 우선

위치가 없는 항목의 배치는 그룹보다 우선하는 독립적인 필드가 아니라 위치 비교의 일부입니다. 따라서 첫 번째 항목에 위치가 없더라도 그룹 0의 항목은 그룹 1의 항목보다 앞섭니다.

이 합성 계산에서는 의도적으로 Swift의 일반 문자열 정렬을 사용합니다. 나중에 클로저를 Foundation 비교자로 바꾸더라도 이 선택을 일관되게 유지하세요.

명시적인 nil 처리를 포함해 여러 필드를 정렬하세요

다음 전체 프로그램은 Swift 표준 라이브러리를 사용하며 가져오기 선언이 필요하지 않습니다. Mac의 Swift 지원 환경에서 실행하세요. 모든 입력은 프로그램에 정의되어 있으며, 계산에 외부 입력이나 영구 저장소는 사용하지 않습니다. 정렬 연산은 sorted(by:)를 통해 새 배열을 반환합니다. Apple의 sorted(by:) 문서

struct ShapeItem {
    let id: Int
    let group: Int
    let preferredPosition: Int?
    let backupPosition: Int?
    let title: String

    var effectivePosition: Int? {
        preferredPosition ?? backupPosition
    }
}

func comesBefore(_ lhs: ShapeItem, _ rhs: ShapeItem) -> Bool {
    if lhs.group != rhs.group {
        return lhs.group < rhs.group
    }

    switch (lhs.effectivePosition, rhs.effectivePosition) {
    case let (.some(left), .some(right)):
        if left != right {
            return left < right
        }
    case (.some(_), .none):
        return true
    case (.none, .some(_)):
        return false
    case (.none, .none):
        break
    }

    if lhs.title != rhs.title {
        return lhs.title < rhs.title
    }

    return lhs.id < rhs.id
}

let items = [
    ShapeItem(id: 10, group: 1, preferredPosition: nil,
              backupPosition: 2, title: "Circle"),
    ShapeItem(id: 20, group: 1, preferredPosition: 1,
              backupPosition: 9, title: "Square"),
    ShapeItem(id: 30, group: 1, preferredPosition: nil,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 40, group: 1, preferredPosition: 2,
              backupPosition: 0, title: "Arc"),
    ShapeItem(id: 50, group: 0, preferredPosition: nil,
              backupPosition: nil, title: "Dot"),
    ShapeItem(id: 60, group: 1, preferredPosition: 2,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 70, group: 1, preferredPosition: 0,
              backupPosition: 8, title: "Zero")
]

let ordered = items.sorted(by: comesBefore)
let expectedIDs = [50, 70, 20, 40, 60, 10, 30]

precondition(ordered.map(\.id) == expectedIDs)
precondition(ordered.count == items.count)
precondition(items.reversed().sorted(by: comesBefore).map(\.id)
             == expectedIDs)

print(ordered.map(\.id))

예상 출력:

[50, 70, 20, 40, 60, 10, 30]

프로그램과 예상 순서는 검토했지만, 조사 환경에 Swift 컴파일러가 없어 여기서 실행하지는 않았습니다. 로컬에서 실행할 때 결과를 확인할 수 있도록 precondition이 포함되어 있습니다.

각 샘플 항목의 위치가 정해지는 이유

항목 50은 위치를 고려하기 전에 그룹 0이 우선하므로 첫 번째에 옵니다. 다음 항목인 70는 0이 유효한 기본 위치임을 보여 줍니다. 이 항목의 보조 값 8은 사용되지 않습니다. 항목 20은 기본 위치 1을 사용합니다.

항목 40, 60, 10은 모두 유효 위치가 2입니다. 제목이 Arc인 두 항목이 Circle보다 앞서고, 식별자가 40와 60 사이의 남은 동률을 해소합니다. 항목 30은 그룹 1에서 두 위치 필드가 모두 비어 있으므로 마지막에 옵니다.

이 규칙은 흔히 묻는 대체 값 관련 질문에도 답합니다. 항목 40의 기본 위치가 다른 항목과 동률일 때 보조 위치 0을 사용하지 않습니다. 이 규칙에서 보조 값은 기본 값이 없을 때만 이를 대체합니다. 보조 값으로도 동률을 해소하려면 비교 규칙을 추가해야 합니다.

값이 같을 때도 비교자가 유효하도록 하세요

Swift는 정렬 조건자가 엄격한 약순서를 형성해야 합니다. 실제로는 어떤 항목도 자기 자신보다 앞서지 않아야 하고, 정렬 결정은 비교 간에 일관되어야 하며, 동등한 항목은 일관된 그룹을 이루어야 합니다. Apple은 sorted(by:)에 대한 요구 사항을 문서화하고 있습니다. 조건자 요구 사항

이 예제는 순서 결과를 반환하기 전에 앞선 키가 다른지 확인합니다. 그룹과 유효 위치가 같을 때만 제목을 비교하고, 제목도 같을 때만 식별자를 비교합니다. 식별자와 앞선 키까지 모두 같으면 마지막 엄격 비교는 false를 반환합니다.

독립적인 필드 비교를 ||로 연결하지 마세요. 한 항목은 그룹 순위가 높지만 위치 순위는 낮고, 다른 항목은 반대일 수 있습니다. OR 기반 조건자는 이때 두 항목이 서로 앞선다고 판단할 수 있습니다. 순차적으로 동률을 처리하면 이러한 모순을 방지할 수 있습니다.

또한 <=을 최종 비교로 사용하지 마세요. 값이 같으면 true를 반환해 항목이 자기 자신보다 앞설 수 없다는 규칙을 위반합니다. 각 비교는 제공된 값과 고정된 정렬 규칙에만 의존해야 합니다.

안정적인 동률 처리와 최종 식별자 비교 중 선택하기

Swift 문서에 따르면 정렬은 안정적입니다. 조건자가 동등하다고 판단한 요소는 입력에서의 상대 순서를 유지합니다. 이것이 원하는 동작이라면 최종 식별자 비교를 생략하고 제목이 같을 때 false을 반환하세요. 안정 정렬 보장

이 예제에서는 고유한 합성 식별자를 사용해 남은 동률을 해소합니다. 따라서 샘플 입력 순서를 뒤집어도 같은 식별자 순서가 나옵니다. 이 속성은 여기 정의한 규칙과 샘플 값에 따른 것입니다. 다른 모든 키가 같은 상태에서 식별자도 반복되면 해당 항목들은 동등한 것으로 처리되어 입력 순서를 유지합니다.

Foundation 키 경로 비교자로 같은 규칙 표현하기

Foundation은 속성을 비교하는 KeyPathComparator와 비교자 시퀀스를 적용하는 sorted(using:)을 제공합니다. 첫 번째 비교자가 기본 순서를 정하고, 이후 비교자는 동률을 세분화합니다. 이 API는 macOS 12부터 사용할 수 있습니다. KeyPathComparator, sorted(using:)

동등한 Foundation 버전에서는 위치 키 두 개, 즉 값 누락 순위와 숫자 위치를 노출합니다. 순위 키가 위치가 없는 항목을 뒤로 보냅니다. 이 순위가 같으면 위치가 없는 항목의 숫자 대체 값이 실제 0과 충돌할 수 없습니다. 두 항목의 순위가 이미 다르기 때문입니다.

위의 전체 프로그램 뒤에 다음 확장과 계산을 추가하세요. 기존 ShapeItem, items, expectedIDs 정의를 사용하는 종속 예제입니다. init(order:)로 ComparableComparator를 명시적으로 생성하므로 macOS 14 이상이 필요합니다. 초기화 메서드 사용 가능 버전

import Foundation

extension ShapeItem {
    var missingPositionRank: Int {
        effectivePosition == nil ? 1 : 0
    }

    var numericPositionKey: Int {
        effectivePosition ?? 0
    }
}

if #available(macOS 14.0, *) {
    let comparators: [KeyPathComparator<ShapeItem>] = [
        KeyPathComparator(\ShapeItem.group, order: .forward),
        KeyPathComparator(\ShapeItem.missingPositionRank,
                          order: .forward),
        KeyPathComparator(\ShapeItem.numericPositionKey,
                          order: .forward),
        KeyPathComparator(
            \ShapeItem.title,
            comparator: ComparableComparator<String>(order: .forward)
        ),
        KeyPathComparator(\ShapeItem.id, order: .forward)
    ]

    let foundationOrdered = items.sorted(using: comparators)
    precondition(foundationOrdered.map(\.id) == expectedIDs)
    print(foundationOrdered.map(\.id))
} else {
    print(items.sorted(by: comesBefore).map(\.id))
}

명시적인 제목 비교자는 앞서 선택한 Swift 문자열 정렬을 유지합니다. Foundation 문서에 따르면 ComparableComparator는 비교 대상 유형의 Comparable 구현을 사용하며, KeyPathComparator는 제공된 속성 비교자를 받을 수 있습니다. ComparableComparator, 키 경로 비교자 초기화 메서드

고정된 하나의 배열 순서에는 클로저를 사용하면 각 분기를 명확히 드러낼 수 있습니다. 지원하는 키의 순서를 값으로 표현하고 싶다면 비교자 배열이 유용합니다. 어느 방식을 사용하든 두 구현이 같은 정의를 사용하도록 모델의 계산 속성에서 대체 값 선택을 처리하세요.

의미를 바꾸지 않고 규칙 조정하기

위치가 없는 항목은 뒤에 두면서 위치를 내림차순으로 정렬하려면 값이 있는 두 위치 간의 비교만 left > right로 바꾸세요. Foundation 버전에서는 numericPositionKey만 역순으로 하고 missingPositionRank는 정방향으로 유지하세요. 전체 결과를 역순으로 뒤집으면 그룹, 제목, 식별자와 위치 누락 배치까지 모두 역순이 됩니다.

선택적 문자열을 대체 키로 사용한다면 빈 문자열을 값이 없는 것으로 취급할지 별도로 정하세요. nil 병합은 nil에 반응하며, 값이 있는 빈 문자열을 대체하지 않습니다. 정렬 전에 추가 정규화 규칙을 정하고 전체 과정에서 같은 유효 키를 사용하세요. nil 병합 동작

이 작은 계산은 동기식으로 유지하세요. Swift 동시성 가이드는 일반 동기식 정렬과 일시 중단 지점을 구분하며, 문서화된 sorted(by:) 조건자는 동기식입니다. 애플리케이션에 비동기 작업의 값이 필요하다면 이 정렬 함수를 호출하기 전에 해당 값을 모두 가져오세요. 비교 클로저 안에 await를 추가하는 방식은 해당 시그니처에서 지원되지 않습니다. 동시성, sorted(by:) 시그니처

이 예제를 다른 데스크톱 목록에 적용하기 전에 기본 키 누락, 위치 키 두 개 누락, 유효한 0, 동일한 유효 위치, 표시 필드 전체가 동률인 경우를 확인하세요. 마지막 동률에서 입력 순서를 유지할지 식별자를 사용할지도 결정하세요. 이렇게 정하면 어느 구현에서도 이해하기 쉬운 정확한 정렬 규칙을 만들 수 있습니다.

출처