Para ordenar um array local do Swift por vários campos, compare cada chave na ordem de prioridade e prossiga somente quando houver empate. Resolva separadamente os valores alternativos opcionais e, em seguida, defina onde os valores ausentes devem ficar. Este exemplo ordena rótulos sintéticos de formas para uma lista da mesa do Mac.

Defina o contrato de ordenação antes de escrever o closure

Suponha que uma lista da mesa contenha rótulos de formas com um número de grupo, uma posição preferencial opcional, uma posição reserva opcional e um título. A tarefa é produzir uma única ordem de exibição previsível a partir desses valores em memória.

Duas decisões precisam de regras separadas. Um valor alternativo escolhe qual valor representa um item: use a posição preferencial quando estiver presente; caso contrário, use a posição reserva. Um critério de desempate compara outro campo somente depois que dois itens têm posições efetivas iguais. O operador de coalescência de nil do Swift, ??, dá suporte à primeira decisão ao selecionar o valor desembrulhado ou o valor alternativo. Operadores básicos — Operador de coalescência de nil

Use este contrato explícito para o exemplo:

Prioridade Chave Regra
1 Grupo Números menores primeiro
2 Posição efetiva preferredPosition ?? backupPosition
3 Posicionamento de posições ausentes Posições ausentes vêm depois das presentes dentro de cada grupo
4 Título Comparação ascendente de strings do Swift quando as posições empatam
5 Identificador Identificadores menores primeiro quando todas as chaves anteriores empatam

O posicionamento de posições ausentes faz parte da comparação de posição, em vez de ser um campo independente que se sobrepõe ao grupo. Um item do grupo zero ainda precede um item do grupo um, mesmo que o primeiro não tenha posição.

A comparação de títulos usa deliberadamente a ordenação comum de strings do Swift para este cálculo sintético. Mantenha essa escolha consistente se depois substituir o closure por comparadores do Foundation.

Escreva uma ordenação completa por vários campos com tratamento explícito de nil

O programa completo a seguir usa a biblioteca padrão do Swift e não exige imports. Execute-o em um ambiente compatível com Swift no seu Mac. Todas as entradas são definidas no programa; o cálculo não usa entradas externas nem armazenamento persistente. A operação de ordenação retorna um novo array por meio de sorted(by:). Documentação da Apple para 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))

Saída esperada:

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

O programa e a ordem esperada foram inspecionados, mas não foram executados aqui porque o ambiente de pesquisa não tem um compilador Swift. As precondições incluídas permitem verificar o resultado ao executá-lo localmente.

Por que cada exemplo fica nessa posição

O item 50 vem primeiro porque o grupo zero tem precedência sobre a posição. Em seguida, o item 70 demonstra que zero é uma posição preferencial válida: seu valor reserva, oito, não é usado. O item 20 usa sua posição preferencial de um.

Os itens 40, 60 e 10 têm todos uma posição efetiva de dois. Os dois títulos Arc vêm antes de Circle, e os identificadores resolvem o empate restante entre 40 e 60. O item 30 vem por último porque os dois campos de posição estão ausentes no grupo um.

Isso também responde a uma dúvida comum sobre valores alternativos: o item 40 não usa sua posição reserva de zero quando sua posição preferencial empata com a de outro item. Segundo este contrato, a posição reserva só substitui um valor preferencial ausente. Se a posição reserva também devesse desempatar, seria necessária uma regra de comparação adicional.

Mantenha o comparador válido quando os valores empatam

O Swift exige que o predicado de ordenação forme uma ordem fraca estrita. Na prática, um item nunca deve preceder a si mesmo, as decisões de ordenação precisam permanecer consistentes entre comparações e itens equivalentes devem formar grupos consistentes. A Apple documenta esses requisitos para sorted(by:). Requisitos do predicado

O exemplo verifica se uma chave anterior difere antes de retornar o resultado da ordenação. Ele chega ao título somente quando o grupo e a posição efetiva empatam; depois, chega ao identificador somente se o título também empatar. Se os identificadores e todas as chaves anteriores forem iguais, a comparação estrita final retorna false.

Evite combinar comparações de campos independentes com ||. Um item pode ter um grupo anterior, mas uma posição posterior; outro pode ter a combinação oposta. Um predicado baseado em OR poderia então afirmar que cada item vem antes do outro. O desempate sequencial evita essa contradição.

Evite também usar <= como comparação final: valores iguais retornariam true, violando a regra de que um item não pode preceder a si mesmo. Mantenha cada comparação dependente apenas dos valores fornecidos e de regras fixas de ordenação.

Escolha entre empates estáveis e um identificador final

A ordenação documentada do Swift é estável: elementos que o predicado considera equivalentes mantêm sua ordem relativa de entrada. Se esse for o comportamento desejado, omita a comparação final de identificadores e retorne false depois do empate nos títulos. Garantia de ordenação estável

Este exemplo usa identificadores sintéticos únicos para resolver os empates restantes. Assim, inverter a entrada de exemplo ainda produz a mesma sequência de identificadores. Essa propriedade decorre deste contrato e dos valores de exemplo. Se os identificadores se repetirem junto com todas as outras chaves, esses itens continuarão equivalentes e manterão sua ordem de entrada.

Expresse as mesmas regras com comparadores de caminhos de chave do Foundation

O Foundation fornece KeyPathComparator para comparar propriedades e sorted(using:) para aplicar uma sequência de comparadores. O primeiro comparador define a ordem principal; os seguintes refinam os empates. Essas APIs estão disponíveis a partir do macOS 12. KeyPathComparator, sorted(using:)

Para uma versão equivalente com Foundation, exponha duas chaves de posição: uma classificação de valor ausente e uma posição numérica. A classificação coloca as posições ausentes por último. Quando essa classificação empata, um substituto numérico para posições ausentes não pode coincidir com um zero presente, pois esses itens já têm classificações diferentes.

Acrescente a extensão e o cálculo a seguir depois do programa completo acima. Este é um exemplo dependente, que usa as definições existentes de ShapeItem, items e expectedIDs. Ele exige o macOS 14 ou posterior porque constrói explicitamente ComparableComparator com init(order:). Disponibilidade do inicializador

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))
}

O comparador explícito de títulos preserva a escolha anterior pela ordenação de strings do Swift. A documentação do Foundation informa que ComparableComparator usa a implementação de Comparable do tipo comparado, e que KeyPathComparator aceita um comparador de propriedade fornecido. ComparableComparator, Inicializador de comparador de caminho de chave

Para uma única disposição fixa, o closure deixa todos os ramos visíveis. Um array de comparadores é útil quando você quer representar como valores a ordem das chaves compatíveis. Em ambas as abordagens, mantenha a seleção do valor alternativo na propriedade computada do modelo para que as duas implementações usem a mesma definição.

Adapte as regras sem alterar seu significado

Para posições em ordem decrescente e posições ausentes por último, altere apenas a comparação entre duas posições presentes para left > right. Na versão com Foundation, inverta somente numericPositionKey; mantenha missingPositionRank na ordem direta. Inverter o resultado inteiro também inverteria grupos, títulos, identificadores e o posicionamento de valores ausentes.

Se uma string opcional se tornar uma chave alternativa, decida separadamente se uma string vazia conta como ausente. A coalescência de nil responde a nil; ela não substitui uma string vazia presente. Resolva qualquer regra adicional de normalização antes de ordenar e use a mesma chave efetiva em todo o processo. Comportamento da coalescência de nil

Mantenha este pequeno cálculo síncrono. O guia de concorrência do Swift distingue a ordenação síncrona comum dos pontos de suspensão, e o predicado documentado de sorted(by:) é síncrono. Se um aplicativo precisar de valores obtidos por trabalho assíncrono, conclua a obtenção desses valores antes de chamar este ordenador. Adicionar await dentro do closure de comparação não é uma extensão compatível com sua assinatura. Concorrência, Assinatura de sorted(by:)

Antes de adaptar o exemplo a outra lista da mesa, mantenha verificações para uma chave preferencial ausente, duas chaves de posição ausentes, um zero válido, posições efetivas iguais e campos de exibição totalmente empatados. Decida se o último empate deve preservar a ordem de entrada ou usar um identificador. Essas escolhas dão à lista um contrato de ordenação preciso e compreensível em qualquer uma das implementações.

Fontes