Stepper closes view from NavigationLink on Increment/Decrement

Viewed 85

So I am working on a phase10 scoreboard app. When you open the app you see a list of games, then you can click on the game and see the players and their scores as well as the rounds that have been logged for the game. When you click on the round I have a tab view that lets you scroll through each player and a stepper that lets you set the points that the player earned for that round. The problem I am having is that when I click on the stepper it closes the view and goes back to the game view. I tried a test with a custom stepper (just a hstack with text and two buttons) but got the same results. So I believe that it is an issue with updating the Round model object but I have no clue what or even how to trace it. It gives me no printout or errors just closes the view and goes back to the game view.

PS: I know that I will get yelled at for copying to much code here, but the last time I posted a question about this app I didn't post stuff and they needed that to compile so that they could recreate. So I just loaded it all its a pretty small app so far. I apologize for the inconvenience but I have no idea which components you would need to debug.

import SwiftUI

@main
struct Phase10App: App {
    
    @ObservedObject var viewModel = ApplicationViewModel()
    
    var body: some Scene {
        WindowGroup {
            GamesView()
                .environmentObject(self.viewModel)
        }
    }
    
}


class ApplicationViewModel: ObservableObject {
    @Published var games: [Game] = []
    
    init() {
        loadDemo()
    }
    
    func loadDemo() {
        for g in 0...3 {
            var game = Game(name: "Demo Game #\(g)")
            for p in 0...3 {
                game.players.append(Player(name: "Player #\(g):\(p)"))
            }
            self.games.append(game)
        }
    }
    
}

struct Game: Identifiable, Codable {
    
    var id: UUID = UUID()
    
    init(name: String) {
        self.name = name
    }
    
    var name: String
    var currentRound: Int = 1
    var players: [Player] = []
    
}

struct Player: Identifiable, Codable {
    
    var id: UUID = UUID()
    
    init(name: String) {
        self.name = name
    }
    
    var name: String
    var rounds: Array<Round> = Array(repeating: Round(), count: 100)
    
}

extension Player {
    var score: Int {
        return self.rounds.map({$0.earnedPoints}).reduce(0, +)
    }
}

struct Round: Identifiable, Codable {
    
    var id: UUID = UUID()
    
    var earnedPoints: Int = 0
    var selectedPhase: Int = 1
    var completedPhase: Bool = false
    
}

struct GamesView: View {
    
    @EnvironmentObject var viewModel: ApplicationViewModel

    var body: some View {
        NavigationView {
            List {
                Section("GAMES") {
                    ForEach(self.viewModel.games.indices) { game in
                        NavigationLink(
                            self.viewModel.games[game].name,
                            destination:
                                GameView(
                                    game: Binding<Game>(
                                        get: {self.viewModel.games[game]},
                                        set: {self.viewModel.games[game]=$0}
                                    )
                                )
                        )
                    }
                    .onDelete(perform: onDelete)
                }
            }
            .navigationTitle("Phase 10")
        }
    }
    
    func onDelete(at offsets: IndexSet) {
        self.viewModel.games.remove(atOffsets: offsets)
    }
    
}

struct GameView: View {
   
    @Binding var game: Game
    
    var body: some View {
        List {
            Section("PLAYERS") {
                ForEach(game.players) { player in
                    HStack {
                        Text(player.name)
                        Spacer()
                        Text("\(player.score)")
                    }
                }
            }
            Section("ROUNDS") {
                ForEach(0..<game.currentRound) { round in
                    NavigationLink(
                        "Round #\(round+1)",
                        destination: View1(game: $game, currentRound: round)
                    )
                }
            }
        }
        .navigationTitle(game.name)
    }
    
    
}

struct View1: View {
    
    @Binding var game: Game
    @State var currentRound: Int
    
    var body: some View {
        TabView {
            ForEach(game.players.indices) { player in
                View1a(
                    player: game.players[player],
                    round: Binding<Round> (
                        get: {game.players[player].rounds[currentRound]},
                        set: {game.players[player].rounds[currentRound] = $0}
                    )
                )
            }
        }
        .tabViewStyle(PageTabViewStyle())
        .navigationTitle("Round #\(currentRound+1)")
    }
    
}

struct View1a: View {
    
    @State var player: Player
    @Binding var round: Round
    
    var body: some View {
        VStack {
            Text(player.name).font(.title)
            Divider()
            Group {
                Stepper(
                    "# of Points: \(self.round.earnedPoints)",
                    value: self.$round.earnedPoints,
                    in: 0...250,
                    step: 5
                )
            }
            .padding(.horizontal, 50)
        }
    }
    
}
2 Answers

The ForEach View can't work with indices it needs to be supplied Identifiable data such as the array of Game structs. e.g.

ForEach(self.viewModel.games) { $game in
    NavigationLink(game.name, destination: GameView(game: $game))
    ...

This is because SwiftUI depends on structural identity, which you can learn more about by watching WWDC 2021 Demystify SwiftUI.

Change

@ObservedObject var viewModel = ApplicationViewModel()

To

@StateObject var viewModel = ApplicationViewModel()
Related