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.