0

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
}
2
  • You could simplify the closures using positional arguments instead of giving them names (eg. for case 1: targetArray.sorted { $0.name > $0.name }) Commented Nov 27, 2017 at 17:51
  • I think Sort Descriptors is the correct approach . - but just doesn't seem to be working Commented Nov 28, 2017 at 14:10

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

4 Comments

That's pretty 😎
Right? KPs are awesome!
Thats awesome but I think NSSortDescriptors are the right approach - as you can also flex the ascending vs descending for Object arrays. - Just doesn't seem to b working
@MobileBloke Added an example passing a comparison function so you have control over the comparison at point of use. Ultimately it's more of an approach difference. NSSortDescriptors is more Type/OOP based, my approach is more functional. One's not especially better than the other.
2

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.