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.