List display going crazy on third sort

Viewed 69

I'm encountering some weird display behavior with the List view and I have no clue about where it comes from.

My List is build using the following code:

List {
    Toggle(isOn: isSortBinding) {
        Text("Tri par points")
    }
    .toggleStyle(SwitchToggleStyle())
    .padding()
                
    ForEach(wordListViewModel.wordList) { word in
        HStack {
           Text( word.word )
           Spacer()
           Text("\( word.points )")
        }
    }
    Spacer()
}
.frame(width: 220)

Where wordList is an Array of a home-made struct ScoredWord:

struct ScoredWord: Comparable, Identifiable, Hashable{
    let word: String
    var points: Int = 0
    let id: UUID

    ...
}

The Toggle is linked to a Binding:

let isSortBinding = Binding<Bool>(
        get: {
            self.isSortedByPoints
        },
        set: {
            self.isSortedByPoints = $0
            wordListViewModel.sortList(isSortedByPoints: isSortedByPoints)
        }
    )

That triggers an insertion sort on the wordList mentioned before.

And here comes the really weird behavior:

  • On the first sort that is done by default (alphabetical), the list is displayed just fine Alpha sort is fine
  • When I use the Toggle to do another sort (by points), the list is also displayed fine Points sort is fine

And that's where it gets funny. If I use the Toggle again, the list starts popping up huge spaces: Going back to alpha sort breaks the list

The method sorting the list is the exact same, with the just attribute used for comparison (word or points). I've tried both to create another sorted Array and to replace my current Array with it, and to sort the current one directly, the issue appears anyways.

Does anyone know what's going on here?

Additional infos:

  • Once the List is "broken", it still puts the elements at a define place when I use the Toggle, as if it was updating an item through its id
  • If the list is long enough for me to scroll, it somehow fixes the display

Here is a complete minimal reproducible example of what is happening.

The view:

import SwiftUI

struct ContentView: View {
    @State private var isSortedByPoints : Bool = false
    
    @ObservedObject var wordListViewModel = WordListViewModel()
    
    var body: some View {
        let isSortBinding = Binding<Bool>(
            get: {
                self.isSortedByPoints
            },
            set: {
                self.isSortedByPoints = $0
                wordListViewModel.sortList(isSortedByPoints: isSortedByPoints)
            }
        )
        
        List {
            Toggle(isOn: isSortBinding) {
                    Text("Tri par points")
            }
            .toggleStyle(SwitchToggleStyle())
            .padding()
            
            ForEach(wordListViewModel.wordList) { word in
                HStack {
                    Text( word.word )
                    Spacer()
                    Text("\( word.points )")
                }
            }
            Spacer()
        }
        .frame(width: 220)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The model:

import Foundation

struct ScoredWord: Comparable, Identifiable{
    let word: String
    var points: Int = 0
    let id: UUID
    
    init(word: String){
        self.id = UUID()
        self.word = word
        self.points = self.defineScore(word: word)
    }
    
    private func defineScore(word: String) -> Int{
        var score : Int
        switch(word.count){
        case 0...2:
            score = 0
        case 3...4:
            score = 1
        case 5:
            score = 2
        case 6:
            score = 3
        case 7:
            score = 5
        default:
            score = 11
        }
        return score
    }
    
    
    /// Following are the methods necessary to comply with Comparable, which is needed for the sorting algorithm in WordListViewModel
    
    static func < (lhs: ScoredWord, rhs: ScoredWord) -> Bool {
        if(lhs.points == rhs.points){
            return lhs.word.count < rhs.word.count
        }
        else{
            return lhs.points < rhs.points
        }
    }

    static func == (lhs: ScoredWord, rhs: ScoredWord) -> Bool {
        return lhs.id == rhs.id
    }
    
}

And the ViewModel:

import Foundation

class WordListViewModel: ObservableObject {
    @Published var wordList: [ScoredWord] = []
    
    init(){
        for _ in 0...50 {
            let random = Int.random(in: 0...1000)
            wordList.append(ScoredWord(word: "TEST\(random)"))
        }
        sortList(isSortedByPoints: false)
    }
    
    
    func sortList(isSortedByPoints: Bool){
        let list = wordList
        let sorted = insertionSort(list, isSortedByPoints: isSortedByPoints)
        wordList = sorted
        
//        insertionSortInOut(&wordList, isSortedByPoints: !isSortedByPoints)
    }
    
    func insertionSort(_ array: [ScoredWord], isSortedByPoints: Bool) -> [ScoredWord] {
        guard array.count > 1 else { return array }

        var sortedArray = array
        for index in 1..<sortedArray.count {
            var currentIndex = index
            let temp: ScoredWord = sortedArray[currentIndex]
            var keepGoing = isSortedByPoints ? temp.points < sortedArray[currentIndex - 1].points : temp.word < sortedArray[currentIndex - 1].word
            while currentIndex > 0, keepGoing {
                sortedArray[currentIndex] = sortedArray[currentIndex - 1]
                currentIndex -= 1
                if(currentIndex > 0){
                    keepGoing = isSortedByPoints ? temp.points < sortedArray[currentIndex - 1].points : temp.word < sortedArray[currentIndex - 1].word
                }
            }
            sortedArray[currentIndex] = temp
        }
        return sortedArray
    }
    
    func insertionSortInOut( _ array: inout [ScoredWord], isSortedByPoints: Bool) {
        guard array.count > 1 else { return }

        for index in 1..<array.count {
            var currentIndex = index
            let temp: ScoredWord = array[currentIndex]
            var keepGoing = isSortedByPoints ? temp.word < array[currentIndex - 1].word : temp.points < array[currentIndex - 1].points
            while currentIndex > 0, keepGoing {
                array[currentIndex] = array[currentIndex - 1]
                currentIndex -= 1
                if(currentIndex > 0){
                    keepGoing = isSortedByPoints ? temp.word < array[currentIndex - 1].word : temp.points < array[currentIndex - 1].points
                }
            }
            array[currentIndex] = temp
        }
    }
    
}

I discovered that the problem disappears when I remove the defineScore method in the struct ScoredWord and replace it with a simple value like self.points = 5.

0 Answers
Related