0

I have a question regarding Swift where I just can't wrap my head around. I have a quite simple view. It's a list where you write down the name of each player and as soon as you hit return the name get's via onCommit appended to the array playerArray. So far this works fine.

Now I want to pass the names that are in this array to another view. How do I do that? Do I use @EnvironmentObject, do I save this to @AppStorage or do I need to use/save it to Core Data and then fetch the data in the view where I need it?

In the next view I would like to show the name of player 1 first and when he finished the game then display the name of player 2 and when he finished the game then the name of player 3 and so on and so on. When everybody played the game I then need all the names again for the scoreboard. While writing this down I guess I also need a way to bound the score each player has achieved to the name.

Later on I would also like to have two teams with two or more players for each team. There the order would be team A, player 1; team B, player 1; team A, player 2; team B, player 2 and so on and so on.

Any hint on how to make this possible would be great. If I need to use Core Data to do that properly then I'll do a deep dive into Core Data.

Thank you!

https://i.stack.imgur.com/EgJ3p.gif

import SwiftUI

struct ContentView: View {
    
    @State var playerField = ""
    @State var playerArray: [String] = []
    
    var body: some View {
        
        VStack {
            List {
                
                ForEach(playerArray, id: \.self) { data in
                    Text(data)
                }
                
                TextField(
                    "Name",
                    text: $playerField,
                    onCommit: {
                        playerArray.append(playerField)
                        playerField = ""
                    }
                )
                
            } // List
        } // VStack
        
        
    } // some View
} // ContentView: View

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
  • You should only use UserDefaults or CoreData if you want to persist this list on disk. There are many ways of doing what you want, it will depend on your view structure, you can pass it to another view as a Binding, you could use EnvironmentObject to access it from anywhere in the app, you can also simply share the same ObservableObject reference between the two views. – Daniel Amarante Jan 25 '22 at 19:35
  • You do not need persistent store (like in `@AppStorage` or in CoreData). You simply need a shared model (e.g. your array). I understand you want to use multiple independent screen, accessing the list of players. So your current `ContentView` is simply the first screen and you want to transition to another e.g. `GameView` later. So you will need some parent view (or simply rename `ContentView` to `AskPlayerNameView`) which will transition between the two views when the list of players has been completed. How do you want to transition between them? Is there a "Start" button in the first screen? – pd95 Feb 04 '22 at 08:13

1 Answers1

0

Here is a somewhat simple solution which ends in a rather huge ContentView but you will get the idea:

struct ContentView: View {

    enum GameState {
        case onboarding
        case running
        case done
    }

    // Shared across views
    @State var gameState = GameState.onboarding
    @State var playerArray: [String] = []

    @State var playerField = ""     // only used in player onboarding

    @State var currentPlayerIdx = 0 // only used while game is running

    var body: some View {
        if case .onboarding = gameState {
            // Player onboarding view (should be an independent view)
            VStack {
                List {

                    ForEach(playerArray, id: \.self) { data in
                        Text(data)
                    }

                    TextField(
                        "Name",
                        text: $playerField,
                        onCommit: {
                            playerArray.append(playerField)
                            playerField = ""
                        }
                    )

                } // List

                Button("Start game") {
                    gameState = .running
                }
                .disabled(playerArray.isEmpty)
            } // VStack Player onboarding view

        } else if case .running = gameState {
            // Game view (should be an independent view)
            VStack {
                Text("\(playerArray[currentPlayerIdx])'s turn")
                    .font(.headline)

                Button("Finish move") {
                    currentPlayerIdx += 1
                    if currentPlayerIdx == playerArray.count {
                        currentPlayerIdx = 0
                    }
                }
                .padding()

                Button("End game") {
                    gameState = .done
                }
            } // VStack Game view
            .padding()

        } else if case .done = gameState {
            // Finished view (should be its own view)
            VStack {
                Button("Start again") {
                    gameState = .onboarding
                }
            } // VStack Game view
        }
    } // some View
} // ContentView: View

I have added a GameState enum to distinguish the games phase (onboarding, running, finished). When updating this enum, the body will generate a different view which is shown to the user.

You see the following problems: basically the ContentView has logic and view layout for three different screens. Some of the @State variables are only needed within a sub-screen. Only the gameState and playerArray are used across multiple screens.

The logical next step is to refactor the code and create three views which are embedded in ContentView depending on the gameState. E.g. for PlayerOnboadingView we will pass in the "shared state" (gameState and playerArray) as a binding to allow it to be modified.

struct PlayerOnboardingView: View {
    @Binding var gameState: ContentView.GameState
    @Binding var playerArray: [String]

    @State var playerField = ""     // only used in player onboarding

    var body: some View {
        VStack {
            List {
                ForEach(playerArray, id: \.self) { data in
                    Text(data)
                }

                TextField(
                    "Name",
                    text: $playerField,
                    onCommit: {
                        playerArray.append(playerField)
                        playerField = ""
                    }
                )

            } // List

            Button("Start game") {
                gameState = .running
            }
            .disabled(playerArray.isEmpty)
        } // VStack Player onboarding view
    }
}

In ContentView you replace the current VStack with

// Player onboarding view
PlayerOnboardingView(gameState: $gameState, playerArray: $playerArray)

and you remove the unused state variable playerField from ContentView.

The same can be done to the other sub-views: create a GameRunningView and a GameFinishedView.

pd95
  • 1,999
  • 1
  • 20
  • 33