Frankie Frankie - 3 months ago 25
iOS Question

Swift sort array of dictionaries by key where value is optional AnyObject

I'm pulling an array of dictionaries straight from Parse and displaying them in a table. So I'd really like to work with the data structure I'm handed (the oddly structured dictionaries below).

A

PFObject
is
[String : AnyObject?]
and I want to be able to sort by any key so I don't know the object type AND the key might be missing from some of the dictionaries. Because in Parse, if you don't give a property a value, it is simply nonexistent. For example:

[
{
"ObjectId" : "1",
"Name" : "Frank",
"Age" : 32
},
{
"ObjectId" : "2",
"Name" : "Bill"
},
{
"ObjectId" : "3",
"Age" : 18
}
{
"ObjectId" : "4",
"Name" : "Susan",
"Age" : 47
}


]

I want the dictionaries with missing keys to always be ordered after the sorted dictionaries. An example:

Original Table:

ObjectId Name Age
1 Frank 32
2 Bill
3 18
4 Susan 47


Ordered By Name:

ObjectId Name Age
2 Bill
1 Frank 32
4 Susan 47
3 18


As I don't have a lot of control over the data model, and it's usage is limited throughout the application, I'd prefer to focus on an algorithmic solution rather than structural.

I came up with a way to do this but it just seems inefficient and slow, I'm certain there's someone who can do this better.

//dataModel is an array of dictionary objects used as my table source
//sort mode is NSComparisonResult ascending or descending
//propertyName is the dictionary key

//first filter out any objects that dont have this key
let filteredFirstHalf = dataModel.filter({ $0[propertyName] != nil })
let filteredSecondHalf = dataModel.filter({ $0[propertyName] == nil })

//sort the dictionaries that have the key
let sortedAndFiltered = filteredFirstHalf { some1, some2 in

if let one = some1[propertyName] as? NSDate, two = some2[propertyName] as? NSDate {
return one.compare(two) == sortMode
} else if let one = some1[propertyName] as? String, two = some2[propertyName] as? String {
return one.compare(two) == sortMode
} else if let one = some1[propertyName] as? NSNumber, two = some2[propertyName] as? NSNumber {
return one.compare(two) == sortMode
} else {
fatalError("filteredFirstHalf shouldn't be here")
}
}

//this will always put the blanks behind the sorted
dataModel = sortedAndFiltered + filteredSecondHalf


Thanks!

Answer

Swift can't compare any two objects. You have to cast them to a specific type first:

let arr: [[String: Any]] = [
    ["Name" : "Frank", "Age" : 32],
    ["Name" : "Bill"],
    ["Age" : 18],
    ["Name" : "Susan", "Age" : 47]
]

let key = "Name" // The key you want to sort by

let result = arr.sort {
    switch ($0[key], $1[key]) {
        case (nil, nil), (_, nil):
            return true
        case (nil, _):
            return false
        case let (lhs as String, rhs as String):
            return lhs < rhs
        case let (lhs as Int, rhs as Int):
            return  lhs < rhs
        // Add more for Double, Date, etc.
        default:
            return true
    }
}

print(result)

If there are multiple dictionaries that have no value for the specified key, they will be placed at the end of the result array but their relative orders are uncertain.