UKDataGeek
UKDataGeek

Reputation: 6922

Sorting a Custom Object by different property values in Swift

I am trying to sort an array of Custom Structs by different property values easily.

struct Customer: Comparable, Equatable {
    var name: String
    var isActive: Bool
    var outstandingAmount: Int
    var customerGroup: String
}

var customerlist: [Customer] // This is downloaded from our backend. 

I want to be able to sort the customerlist array in the UI by all the field values when the user selects the various icons.

I have tried a few methods to sort it using a switch statement - however I am told that the correct way to do this is using Sort Descriptors( which appear to be Objective-C based and mean I need to convert my array to an NSArray. ) I keep getting errors when I try this approach with native Swift structs.

What is the best way to allow the user to sort the above array using Swift?

Eg: below seems very verbose!

func sortCustomers(sortField:ColumnOrder, targetArray:[Customer]) -> [Customer] { //Column Order is the enum where I have specified the different possible sort orders
        var result = [Customer]()
    switch sortField {
        case .name:
             result = targetArray.sorted(by: { (cust0: Customer, cust1: Customer) -> Bool in
                return cust0.name > cust1.name
            })
        case .isActive:
             result = targetArray.sorted(by: { (cust0: Customer, cust1: Customer) -> Bool in
                return cust0.isActive > cust1.isActive
            })
        case .outstandingAmount:
            result = targetArray.sorted(by: { (cust0: Customer, cust1: Customer) -> Bool in
                return cust0.outstandingAmount > cust1.outstandingAmount
            })
        case .customerGroup:
            result = targetArray.sorted(by: { (cust0: Customer, cust1: Customer) -> Bool in
                return cust0.customerGroup > cust1.customerGroup
            })
    }
    return result
}

Upvotes: 0

Views: 899

Answers (2)

Jeffery Thomas
Jeffery Thomas

Reputation: 42598

I re-packaged the verbose solution to make something nicer. I added a property to ColumnOrder that returns a ordering closure.

struct Customer {
    var name: String
    var isActive: Bool
    var outstandingAmount: Int
    var customerGroup: String
}

enum ColumnOrder {
    case name
    case isActive
    case outstandingAmount
    case customerGroup

    var ordering: (Customer, Customer) -> Bool {
        switch self {
        case .name:              return { $0.name > $1.name }
        case .isActive:          return { $0.isActive && !$1.isActive }
        case .outstandingAmount: return { $0.outstandingAmount > $1.outstandingAmount}
        case .customerGroup:     return { $0.customerGroup > $1.customerGroup }
        }
    }
}

Here is how it's used:

let sortedCustomers = customers.sorted(by: ColumnOrder.name.ordering)

Next, I extended Sequence to make calling it from an array look good.

extension Sequence where Element == Customer {
    func sorted(by columnOrder: ColumnOrder) -> [Element] {
        return sorted(by: columnOrder.ordering)
    }
}

Final result:

let sortedCustomers = customers.sorted(by: .name)

Upvotes: 2

GetSwifty
GetSwifty

Reputation: 7756

What I would go with, is using KeyPaths:

func sortCustomers<T: Comparable>(customers: [Customer], with itemPath: KeyPath<Customer, T>) -> [Customer] {
    return customers.sorted() {
       $0[keyPath: itemPath] < $1[keyPath: itemPath]
    }
}

This approach avoids the need for your enum at all, and allows you to just do

let testData = [Customer(name: "aaaa", isActive: false, outstandingAmount: 1, customerGroup: "aaa"),
                Customer(name: "bbbb", isActive: true, outstandingAmount: 2, customerGroup: "bbb")];

let testResultsWithName = sortCustomers(customers: testData, with: \Customer.name)
let testResultsWithActive = sortCustomers(customers: testData, with: \Customer.isActive) 
// etc

Notice that I switched the > to a <. That is the default expectation and will result in "a" before "b", "1" before "2", etc.

Also, you need to add an extension for Bool to be comparable:

extension Bool: Comparable {
    public static func <(lhs: Bool, rhs: Bool) -> Bool {
        return lhs == rhs || (lhs == false && rhs == true)
    }
}

To round out the approach, you can also pass in a comparison function:

func sortCustomers<T: Comparable>(customers: [Customer], comparing itemPath: KeyPath<Customer, T>, using comparitor: (T, T) -> Bool) -> [Customer] {
    return customers.sorted() {
        comparitor($0[keyPath: itemPath], $1[keyPath: itemPath])
    }
}
let testResults = sortCustomers(customers: testData, comparing: \Customer.name, using: <)

This way you can use the normal comparison operators: (<, <=, >, >=) as well as a closure if you want custom sorting.

Upvotes: 2

Related Questions