0

I'm working through a tutorial on NSTableView with my own data. I've gotten to the point where I'm writing my sorting function, and this is what I've come up with:

var payment_list: [Payment]!

func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
    guard let sortDescriptor = tableView.sortDescriptors.first else {
        return
    }

    let key = sortDescriptor.key!

    if sortDescriptor.ascending == true {
        if key == "id" {
            payment_list.sort { $0.id < $1.id }
        } else if key == "msatoshi" {
            payment_list.sort { $0.msatoshi < $1.msatoshi }
        } else if key == "created_at" {
            payment_list.sort { $0.created_at < $1.created_at }
        } else if key == "status" {
            payment_list.sort { $0.status < $1.status }
        }
    } else {
        if key == "id" {
            payment_list.sort(by: { $0.id > $1.id })
        } else if key == "msatoshi" {
            payment_list.sort { $0.msatoshi > $1.msatoshi }
        } else if key == "created_at" {
            payment_list.sort { $0.created_at > $1.created_at }
        } else if key == "status" {
            payment_list.sort { $0.status > $1.status }
        }
    }

    tableView.reloadData()
}

It works fine, but I can't help but thinking that this is pretty verbose for something as common as sorting a multi-column list. The actual list in my app has several additional columns.

Are there any Swift tricks that I can use to make this code a bit more concise? Something like this would be more desirable:

if sortDescriptor.ascending == true {
    payment_list.sort($0[key] < $1[key])
} else {
    payment_list.sort($0[key] > $1[key])
}

Or maybe even something like:

payment_list = payment_list.sort(by: key, order: "ASC")

I get the impression that there are a lot of giant nested if...else if and/or switch...case blocks involved in Swift development. Is that an accurate assessment?

Update:

This is my Payment definition:

struct Payment: Codable, PaymentFactory {
    let id: Int
    let payment_hash: String
    let destination: String
    let msatoshi: Int
    let timestamp: Int
    let created_at: Int
    let status: String

    enum PaymentKeys: String, CodingKey {
        case payments
    }

    static func fake() -> Payment {
        // snipped for brevity
    }
}
5
  • Exactly, why don't you use switch constructs? Commented Jan 29, 2018 at 23:41
  • Preference of switch vs if...else doesn't change the overall verbosity of the code. It would actually add eight lines of code using standard formatting. Commented Jan 29, 2018 at 23:49
  • What does your Payment class look like? What type are all your attributes? Commented Jan 29, 2018 at 23:54
  • You check the value of key, and it can have one of different values. I think control statements are the only way Commented Jan 29, 2018 at 23:54
  • Are you using Swift 4? Commented Jan 29, 2018 at 23:58

2 Answers 2

1

What about adding an Enum for handling your various sort descriptor keys? Then you can add a function to your Payment class that returns the correct attribute based on the enum value passed in, and this makes your actual sort function very concise.

If all your attributes were Ints, for example, you could do something like this:

enum Key: String {
    case id = "id", amount = "amount", createdAt = "createdAt", status = "status"
}

class Payment {

  let id: Int
  let amount: Int
  let createdAt: Int
  let status: Int

  func attribute(forKey key: Key) -> Int {
    switch key {
    case .id: return self.id
    case .amount: return self.amount
    case .createdAt: return self.createdAt
    case .status: return self.status
    }
  }

}

And then when it comes time to sort, you could do this:

let key = Key(rawValue: sortDescriptor.key!)!

if sortDescriptor.ascending {
  paymentList.sort(by: { $0.attribute(forKey: key) < $1.attribute(forKey: key) })
} else {
  paymentList.sort(by: { $0.attribute(forKey: key) > $1.attribute(forKey: key) })
}

Or you could even turn the sorting part into a one-liner with a ternary operator:

paymentList.sort(by: { sortDescriptor.ascending ? $0.attribute(forKey: key) < $1.attribute(forKey: key) : $0.attribute(forKey: key) > $1.attribute(forKey: key) })

Of course this becomes slightly more complicated if your attributes are of different types, but this method would still be usable - you'd really just have to change the attribute(forKey: method to return a Comparable or something else.

At first glance I realize the Enum for Key looks like overkill, but I think it'd be better do handle only valid keys when picking an attribute and sorting, and deal with the possibility of an invalid input at the time that you first get the key from the sortDescriptor.

EDIT: Since you are using different multiple types for your attributes it might be easier just to make a function that compares 2 Payment instances based on a key, and use that to sort. My updated implementation looks like this:

enum Key: String {
    case id = "id", amount = "amount", createdAt = "createdAt", status = "status"
}

class Payment {

    let id: Int
    let amount: Int
    let createdAt: Int
    let status: String

    func compare(toOther other: Payment, byKey key: Key, ascending: Bool) -> Bool {
        switch key {
        case .id:
            return ascending ? self.id < other.id : self.id > other.id
        case .amount:
            return ascending ? self.amount < other.amount : self.amount > other.amount
        case .createdAt:
            return ascending ? self.createdAt < other.createdAt : self.createdAt > other.createdAt
        case .status:
            return ascending ? self.status < other.status : self.status > other.status
        }
    }
}

This would make your sort look like this:

let sortDescriptor = NSSortDescriptor(key: "amount", ascending: true)

let key = Key(rawValue: sortDescriptor.key!)!

paymentList.sort(by: { $0.compare(toOther: $1, byKey: key, ascending: sortDescriptor.ascending) })
Sign up to request clarification or add additional context in comments.

2 Comments

This is really close to what I'm looking for, but I also have strings in my object. I've updated the question.
@BenHarold See my edit. I didn't add every key to my example, but this works well.
0

If your Payment type is an NSObject you could tag the data fields with @objc and use NSSortDescriptor's built in comparison function. It will even handle ascending/descending automatically.

Working playground snippet:

import Foundation

class Payment: NSObject {
    @objc var id: String
    @objc var amount: Double
    @objc var created_at: Date
    @objc var status: String

    init(id: String, amount: Double, created_at: Date, status: String) {
        self.id = id
        self.amount = amount
        self.created_at = created_at
        self.status = status
        super.init()
    }
}

let arr: [Payment] = [
    Payment(id: "1", amount: 123, created_at: Date(), status: "abc"),
    Payment(id: "5", amount: 123, created_at: Date(), status: "abc"),
    Payment(id: "4", amount: 123, created_at: Date(), status: "abc"),
    Payment(id: "2", amount: 123, created_at: Date(), status: "abc")
]

let sd = NSSortDescriptor(key: "id", ascending: true)

let sorted = arr.sorted { sd.compare($0, to: $1) == .orderedAscending }

dump(sorted)

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.