Reputation: 24466
As a discipline to just learn the Swift way of doing things I try avoid as much typecasting to NSWhatevers as possible, however, there are times like these where doing so is quite frustrating.
I have a table view controller with sortable headers for columns of data. The data I use is an array of objects. I want to sort the array by the field names in those objects. If the user taps the same column header button again, the sort order is reversed which means I have to keep track of the current sort direction for each field/column.
In Objective-C/NSArray APIs, it was pretty simple to do this dynamically. You just use an NSSortDescriptor
with sortedArrayUsingDescriptors
on NSArray
passing it the field/column name and the sort order and you got back what you need.
In Swift however, you cannot pass the field name dynamically as a string. Instead you have to specify explicitly which field you want to use which means you either have to setup a verbose switch or if/else construct or you have to define a dictionary that takes your sort field as the key and a closure as the value. The closure then is what gets passed to your sort function. I set this up in a playground just to see what I could figure out going with the dictionary/closure lookup route. Here's what I came up with using some dummy objects:
class Order {
var orderNumber:String
var invoiceNumber:String
var orderDate:NSDate
init(orderNumber:String, invoiceNumber:String, orderDate:NSDate) {
self.orderNumber = orderNumber
self.invoiceNumber = invoiceNumber
self.orderDate = orderDate
}
}
I instantiate a few of them in an array like this:
var orders = [Order(orderNumber: "123456", invoiceNumber: "2342342", orderDate: NSDate()),
Order(orderNumber: "332422", invoiceNumber: "848484", orderDate: NSDate()),
Order(orderNumber: "423425", invoiceNumber: "488244", orderDate: NSDate())]
Then I create my lookup dictionary that provides the closure I want to sort with:
var sorters = [
"orderNumber" :
[true : { (o1: Order, o2: Order) -> Bool in return o1.orderNumber < o2.orderNumber },
false : { (o1: Order, o2: Order) -> Bool in return o1.orderNumber > o2.orderNumber }],
"invoiceNumber" :
[true : { (o1: Order, o2: Order) -> Bool in return o1.invoiceNumber < o2.invoiceNumber },
false : { (o1: Order, o2: Order) -> Bool in return o1.invoiceNumber > o2.invoiceNumber }],
"orderDate" :
[true : { (o1: Order, o2: Order) -> Bool in return o1.orderDate < o2.orderDate },
false : { (o1: Order, o2: Order) -> Bool in return o1.orderDate > o2.orderDate }]
]
You're probably wondering what gives with the true
and false
keys. This is to determine whether it should be sorted ascending (true) or descending (false). You can see how this plays out when I go to actually run the sort:
var currentSortField = "invoiceNumber"
var ascending = false
orders.sort(sorters[currentSortField]![ascending]!)
Note: I had to also overload the greater than and less than operators for NSDate to get the comparison to work in the "orderDate" field:
func >(lhs:NSDate, rhs:NSDate) -> Bool {
return lhs.compare(rhs) == NSComparisonResult.OrderedAscending
}
func <(lhs:NSDate, rhs:NSDate) -> Bool {
return lhs.compare(rhs) == NSComparisonResult.OrderedDescending
}
The background on this question I realize is a bit long, however, I am just a bit befuddled that Swift doesn't appear to have a more elegant solution for solving this problem and wanted to provide the complete solution I've come up with to date. Remember, the NSWhatever equivalent in this case is a one liner:
var sorted = (orders as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: currentSortField, ascending: ascending)])
So here's the question. Is there a more succinct, expressive, and idiomatic way to do this in Swift without resorting to Foundation APIs?
If you want to try out the playground I created to test this, you can get it here: https://www.dropbox.com/s/8m53bprzui50muq/Sorting.playground.zip?dl=0
UPDATE After a discussion with Matt in the comments, I went ahead and implemented a sorting closure that compromises by using valueForKey:
after making my Order object inherit from NSObject. It is a compromise, but it keeps me from completely abandoning the Swift sort function altogether and yet I don't have to type a boat ton of code. It goes like this:
var currentSortField = "invoiceNumber"
var ascending = false
var sorter = { (o1: Order, o2: Order) -> Bool in
if ascending {
return (o1.valueForKey(currentSortField) as String) < (o2.valueForKey(currentSortField) as String)
} else {
return (o1.valueForKey(currentSortField) as String) > (o2.valueForKey(currentSortField) as String)
}
}
orders.sort(sorter)
It's no one-liner, but it's an interesting compromise.
UPDATE TO THE UPDATE
That's actually not going to work unless all of the vars are strings--which they are not. This dies on o1.valueForKey(currentSortField) as String
when the field in question is an NSDate and not a String.
Upvotes: 1
Views: 800
Reputation: 783
Try this solution. It works and the shortest way
var array = [6,3,2,1,5,4]
Sort (ascending) its elements
array = array.sort { $0 < $1 }
print(array)
result is [1, 2, 3, 4, 5, 6]
Upvotes: 0
Reputation: 535232
It isn't clear to me what kind of answer would satisfy you. Personally I think your solution is a good one. A more simple-minded version, for those having trouble following what you're doing, would be something like this:
struct Thing {
var first:String
var last:String
}
var arr = [Thing]()
arr.append(Thing(first:"Al", last:"Zork"))
arr.append(Thing(first:"Zelda", last:"Aardvark"))
var howToSort: ((Thing, Thing) -> Bool)
func sorter(thing1:Thing, thing2:Thing) -> Bool {
return thing1.first < thing2.first
}
howToSort = sorter
let arr2 = sorted(arr, howToSort)
func sorter2(thing1:Thing, thing2:Thing) -> Bool {
return thing1.last < thing2.last
}
howToSort = sorter2
let arr3 = sorted(arr, howToSort)
The point is simply that things like sorter
and sorter2
are things that can be stored and retrieved. And that's what saving the sort criteria depends on.
Now, it's also true that sorter
and sorter2
can't be formed dynamically, the way an NSSortDescriptor can, or applied dynamically and type-agnostically, the way KVC is doing when you sort using a sort descriptor. But Swift, as I said in my comment, is a language that deliberately lacks eval ("don't be eval!"), introspection, dynamic message formation (no performSelector
), and type-agnosticism. You can generalize your solution somewhat using a generic, I suppose, but the real issue seems to me to be that you're now addicted to the "power" of those things and you want to go cold-turkey on them without actually giving them up. My job, as I see it, is to talk you off the ledge.
Of course, my real response to your question is: Hey, Objective-C and NSSortDescriptor are not going away, so what's the big deal?
Upvotes: 1