Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Nested ForEach causes unexpected ordering

I am using two ForEach() in a nested way, one to display the sections, and the other to display the individual cells of the List view.

My problem is that the iterator variable from the first ForEachis behaving in a very weird way when accessing it in the second ForEach.

enter image description here

In this gif I attached, the first variable behind Round (ri in the code snippet below) changes its value sometimes when I scroll up or down. However the value of this variable is always the same inside the first ForEach, as we can see by the section name where the displayed round is always the correct one.

Here is the code with the nested ForEach statements. However as it is presented here, you'll probably not manage to reproduce the bug. While maybe superstitiously, it seems that the more complex my View is, the more likely the bug is to appear.

        List {
            ForEach(Array(self.game.rounds.indices), id: \.self) { ri in
                Section(header: Text("Round \(ri): \(self.game.rounds[ri])")) {
                    ForEach(Array(self.game.players.indices), id: \.self) { pi in
                        HStack {
                            Text("\(self.game.players[pi])")
                            Text("Round \(ri), Player \(pi)")
                        }
                    }
                }
            }
        }

Here is the complete view with the nested ForEachthat causes the problems for me:

https://github.com/charelF/carioca/blob/master/unote/GameView.swift

I'm fairly certain that what I am encountering is actually a bug in SwiftUI. However I'm not sure, and this behaviour may be expected behaviour or some kind of asynchronous loading of cells.


Edit 1:

Either I'm doing something wrong, or it seems that the answer by @Asperi does not really solve the problem. Here's a quick and dirty workaround that I tried to avoid the id's being the same

        List {
            ForEach([10,11,12,13,14,15,16,17,18,19] /*Array(self.game.rounds.enumerated())*/, id: \.self) { ri in
                Section(header: Text("Round \(ri-10): \(self.game.rounds[ri-10])")) {
                    ForEach(Array(self.game.players.indices), id: \.self) { pi in
                        HStack {
                            Text("ri \(ri), pi \(pi)")
                        }
                    }
                }
            }

The id's are actually never the same, however the rows/cells are still messed up, as seen on this screenshot...

Moreover I'm struggling to find a good way to iterate over these two arrays, given that I need the indices, so I can really iterate over the arrays themselves. The only solutions I can think of is either something ugly like the one above, using .enumerate() (which I tried, and got similar error as above), or maybe transforming my arrays into dictionaries with unique keys...


Edit 2:

I believe I understand the problem. However I have no idea how to solve it. I tried to create custom structs for both Round and Player instead of the Int that I used previously. This means I can write my nested ForEach as follows:

        List {
            ForEach(self.game.rounds, id: \.id) { round in
                Section(header: Text("\(round.number): \(round.desc)")) {
                    ForEach(self.game.players, id: \.id) { player in
                        Text("\(player.name), \(round.number)")
                    }
                }
            }
        }

This required rewriting the logic behind Game.swift https://github.com/charelF/carioca/blob/master/unote/Game.swift

Unfortunately this has not solved the problem. The order is still messed up. Even though I believe I have understood the issue, I don't know how to solve it any other way.


Edit 3:

I tried the suggested answer here (Nested ForEach (and List) in Views give unexpected results) (enclosing the view inside the outer ForEach with a group), however the error still persists


Edit 4:

I finally got it to work, unfortunately my solution is more of a hack and really not very clean. The solution is indeed as in the answer, all the cells need unique identifiers.

List {
    ForEach(self.game.rounds) { round in
        Group {
            Section(header: Text("\(round.number): \(round.desc)")) {
                ForEach(self.game.scoreBoard[round]!, id: \.self.id) { sbe in
                    Text("\(round.number) \(sbe.id)")
                    
                }
            }
        }
    }
}

As seen in the screenshot below, the 4 cells below round 7 have different identifiers than the 4 cells below round 8, which, if I understand correctly, solves my problem.

Unfortunately the solution required some, imo, ugly workarounds in my game logic. https://github.com/charelF/carioca/blob/master/unote/Game.swift

like image 305
charelf Avatar asked Jul 16 '20 08:07

charelf


1 Answers

It fails due to non-unique identifiers for rows. It is used index, but it is repeated in different sections, so List row reuse caching engine confused.

In the described scenario it should be selected something unique for items that are shown in rows, like (scratchy)

ForEach(self.game.rounds) { round in  // make round identifiable
      // ...
        ForEach(self.game.players) { player in // make player identifiable

and rounds ids should not overlap with player ids as well.

like image 98
Asperi Avatar answered Nov 12 '22 11:11

Asperi