3

I'm having a heck of a time in Swift 3 sorting an array of dictionaries.

In Swift 2, I would do it this way, which worked fine:

var dicArray = [Dictionary<String, String>()]
let dic1 = ["last": "Smith", "first": "Robert"]
dicArray.append(dic1)
let dic2 = ["last": "Adams", "first": "Bill"]
dicArray.append(dic2)
let sortedArray = dicArray.sort { ($0["last"] as? String) < ($1["last"] as? String) }

Converting the same code to Swift 3 has not gone well. The system guided me to this (through a circuitous route):

let sortedArray = dicArray.sorted { ($0["last"]! as String) < ($1["last"]! as String) }

But the app always crashes, with the error that it found nil while unwrapping an Optional value.

After banging my head against the table for too long, putting ?s and !s in every imaginable combination, I resorted to an old approach to get the job done:

let sortedArray = (dicArray as NSArray).sortedArray(using: [NSSortDescriptor(key: "last", ascending: true)]) as! [[String:AnyObject]]

That works, and I'm moving along, but it's not very Swifty, is it?

Where did it all go wrong? How can I make the pure Swift sort function work in a case like this?

2
  • 2
    If you have a static set of keys, you should definitely use a struct rather than a dict. Commented Dec 31, 2016 at 0:17
  • 2
    As @Alexander writes, use a struct: if the keys are not static, the sorting above is very fragile w.r.t. runtime safety, and it should be verified that all dictionaries in the arrays of dictionaries indeed does contain the key "last", e.g. checking such that dicArray.contains(where: { !$0.keys.contains("last") }) is not true. If you had incorporated such a guard in the first place, you would've caught the empty additional dictionary, as this would've violated the the input verification. Commented Dec 31, 2016 at 0:20

3 Answers 3

11

Where did it all go wrong?

It went wrong on your first line:

var dicArray = [Dictionary<String, String>()]

That never did what you want, even in Swift 2, because you are actually inserting an extra, empty dictionary into the array. That's where the crash comes from; the empty dictionary has no "last" key, because it is, uh, empty.

You want this:

var dicArray = [Dictionary<String, String>]()

See the difference? After that change, everything falls into place:

var dicArray = [Dictionary<String, String>]()
let dic1 = ["last": "Smith", "first": "Robert"]
dicArray.append(dic1)
let dic2 = ["last": "Adams", "first": "Bill"]
dicArray.append(dic2)
let sortedArray = dicArray.sorted {$0["last"]! < $1["last"]!}
// [["first": "Bill", "last": "Adams"], ["first": "Robert", "last": "Smith"]]
Sign up to request clarification or add additional context in comments.

1 Comment

I can't believe I did that. Good eye. Thanks so much.
1

Rather than using dictionaries with a fixed set of keys, it's generally advisable to create your own custom types:

struct Person {
    let lastName: String
    let firstName: String
}

This way, you never have to worry about whether you got the key for a particular value in a dictionary right, because the compiler will enforce checks for the names of the property. It makes it easier to write robust, error-free code.

And, coincidentally, it makes sorting cleaner, too. To make this custom type sortable, you make it conform to the Comparable protocol:

extension Person: Comparable {
    public static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.lastName == rhs.lastName && lhs.firstName == rhs.firstName
    }

    public static func < (lhs: Person, rhs: Person) -> Bool {
        // if lastnames are the same, compare first names, 
        // otherwise we're comparing last names

        if lhs.lastName == rhs.lastName {
            return lhs.firstName < rhs.firstName
        } else {
            return lhs.lastName < rhs.lastName
        }
    }
}

Now, you can just sort them, keeping the comparison logic nicely encapsulated within the Person type:

let people = [Person(lastName: "Smith", firstName: "Robert"), Person(lastName: "Adams", firstName: "Bill")]
let sortedPeople = people.sorted()

Now, admittedly, the above dodges your implicit question of how to compare optionals. So, below is an example where firstName and lastName are optionals. But, rather than worrying about where to put the ? or !, I'd use nil-coalescing operator, ??, or a switch statement, e.g.:

struct Person {
    let lastName: String?
    let firstName: String?
}

extension Person: Comparable {
    public static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.lastName == rhs.lastName && lhs.firstName == rhs.firstName
    }

    public static func < (lhs: Person, rhs: Person) -> Bool {
        // if lastnames are the same, compare first names, 
        // otherwise we're comparing last names

        var lhsString: String?
        var rhsString: String?
        if lhs.lastName == rhs.lastName {
            lhsString = lhs.firstName
            rhsString = rhs.firstName
        } else {
            lhsString = lhs.lastName
            rhsString = rhs.lastName
        }

        // now compare two optional strings

        return (lhsString ?? "") < (rhsString ?? "")

        // or you could do
        // 
        // switch (lhsString, rhsString) {
        // case (nil, nil): return false
        // case (nil, _): return true
        // case (_, nil): return false
        // default: return lhsString! < rhsString!
        // }
    }
}

The switch statement is more explicit regarding the handling of nil values (e.g. nil sorted before or after non-optional values) and will distinguish between a nil value and an empty string, should you need that. The nil coalescing operator is simpler (and IMHO, more intuitive for the end-user), but you can use switch approach if you want.

1 Comment

this is more than just answer, thanks for sharing the knowledge :)
0

let descriptor: NSSortDescriptor = NSSortDescriptor.init(key: "YOUR KEY", ascending: true)

let sortedResults: NSArray = tempArray.sortedArray(using: [descriptor]) as NSArray

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.