Home > Software design >  Swift sorted(by:) method doesn't correctly sort an array of struct based on integer
Swift sorted(by:) method doesn't correctly sort an array of struct based on integer

Time:03-16

I need to sort an array of some custom struct objects. My structure contains a "user_id" Int value, which is the one I have to use to sort the array. So far, nothing really exciting.

And then I have to force one item (based on its user_id) to stay on top of my list, ie. as the first element of my sorted array.

I came with the current code:

myArray.sorted(by: { $0.user_id == 40 ? true : $0.user_id < $1.user_id })

but sometimes (not always) the array seems to totally ignore the first comparison and just sort items based on their user_id. So for example if my list has the elements 37, 40, 41, it'll be either sorted as [40, 37, 41] or [37, 40, 41] and I can't find why or how.

For the context: I use this list in a dynamic var (I forgot how they are called) in a SwiftUI view, which uses a ForEach(mySortedArray) to build a VStack of sorted custom rows. So basically the SwiftUI code looks like that (BoardMember being my custom struct):

    @State private var boardPerms: [BoardMember] = []

    func fetchMembers() {
        boardPerms = ...
    }

    var displayedMembers: [BoardMember] {
        boardPerms.sorted(by: { $0.user_id == 40 ? true : $0.user_id < $1.user_id })
    }

    var body: some View {
    VStack {
        ForEach(displayedMembers) { member in
            ...
        }
        .onAppear(perform: fetchMembers)
    }

and I can assure the issue is in the displayedMembers var, because I also tried to debug it with the following code inside my body: Text((displayedMembers.map{$0.user_id.description}).joined(separator: " "))

Any idea what's wrong with my comparison? Thanks for the help!

CodePudding user response:

The predicate to Array.sorted(by:) requires that you

return true if its first argument should be ordered before its second argument; otherwise, false.

The order of the arguments passed to the predicate can be given in either order ((a, b) or (b, a)) depending on how the list has been sorted so far, but the predicate must give a consistent result in either order or else you'll get nonsense results.

In your predicate, you're checking the first element's user ID to prioritize it, but not the second; i.e., you're handling (a, b) but not (b, a). If you update your predicate to

myArray.sorted(by: {
    // The order of the checking here matters. Specifically, the predicate
    // requires a `<` relationship, not a `<=` relationship. If both $0 and $1
    // have the same `user_id`, we need to return `false`. Checking $1.user_id
    // first hits two birds with one stone.
    if $1.user_id == 40 {
        return false
    } else if $1.user_id == 40 {
        return true
    }
    
    return $0.user_id < $1.user_id
})

then you'll get consistent results.

Alternatively, if the element is known ahead of time, you can avoid this by extracting it from the list and sorting the remaining results.

  • Related