UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to add items to an array in a struct and iterate over it?

Forums > SwiftUI

Hello everyone,

following the videos und tutorials from HackingWithSwift I did my very first steps in Swift and SwiftUI. I ended up with a very simple List of Players for my Team and a simple edit-View to make changes. The app itself works fine.

Now I started to use a NavigationSplitView to add a Team-Layer in the sidebar. I want to add and delete teams and in addition I want to add and delete players to the teams.

Actually I am struggeling over the "ForEach" in the "contet:" part of my ContentView. Question 1: How can I iterate over the teamMembers in my teamsList?

Second: in my PlayerAddView I hava a button to add players to the team. Question 2: How can I append players to my teamsList.teamMembers-array?

Hope these are not to stupid beginner questions...

Here the full code:

import SwiftUI

//Team and Player Struct and TeamsList Class

struct Team: Identifiable, Codable, Hashable {
    var id = UUID()
    var name: String
    var teamMembers: [Player]
}

struct Player: Identifiable, Codable, Hashable {
    var id = UUID()
    var firstName: String
    var lastName: String
}

class TeamsList: ObservableObject {
    @Published var teams: [Team] = []

    @MainActor class TeamsList: ObservableObject {
        @Published var teams = [Team]() {
            didSet {
                if let encoded = try? JSONEncoder().encode(teams) {
                    UserDefaults.standard.set(encoded, forKey: "Teams")
                }
            }
        }

        init() {
            if let savedItems = UserDefaults.standard.data(forKey: "Teams"){
                if let decodedItems = try? JSONDecoder().decode([Team].self, from: savedItems) {
                    teams = decodedItems
                    return
                }
            }
            teams = []
        }
    }
}

struct ContentView: View {

    @StateObject var teamsList = TeamsList()

    @State private var showingAddPlayer = false
    @State private var showingAddTeam = false

    @State private var selectedTeam: Team?
    @State private var selectedPlayer: Player?

    var body: some View {
        NavigationSplitView {
            List(selection: $selectedTeam){

                ForEach(teamsList.teams) { team in
                    NavigationLink(team.name, value: team)
                }
                .onDelete(perform: removeTeam)
            }
            .navigationTitle("Teams")
            .toolbar{
                EditButton()

                Button{
                    showingAddTeam = true
                } label: {
                    Image(systemName: "person.badge.plus")
                }

                .sheet(isPresented: $showingAddTeam) {
                    TeamAddView()
                }

            }
        } content: {
            if selectedTeam != nil {
                List(selection: $selectedPlayer){

                    // How to iterrate over the teamMembers in my teamList-struct?
                    ForEach(0..<3) { player in
                        NavigationLink("Hello \(player)", value: player)
                    }
                }
                .navigationTitle(selectedTeam?.name ?? "Player")

                .toolbar{
                    EditButton()

                    Button{
                        showingAddPlayer = true
                    } label: {
                        Image(systemName: "person.badge.plus")
                    }

                    .sheet(isPresented: $showingAddPlayer) {
                        PlayerAddView()
                    }

                }
            } else {
                VStack {
                    Image(systemName: "person.3.fill")
                    Text("Select a Team")
                }
            }

        } detail: {
            PlayerDetailView(player: selectedPlayer)
        }
        .environmentObject(teamsList)
    }

    func removeTeam(at offsets: IndexSet) {
        teamsList.teams.remove(atOffsets: offsets)
    }
}

// Team Add View
struct TeamAddView: View {

    @EnvironmentObject var teamsList: TeamsList

    @Environment(\.dismiss) var dismiss

    @State private var name = ""

    var body: some View {
        NavigationStack {
            Form{
                TextField("Team Name", text: $name)

            }
            .navigationTitle("Add new Player")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

                    let team = Team(name: name, teamMembers: [])
                    teamsList.teams.append(team)
                    dismiss()
                }
            }
        }
    }
}

struct PlayerAddView: View {

    @Environment(\.dismiss) var dismiss

    @State private var firstName = ""
    @State private var lastName = ""

    var body: some View {
        NavigationStack {
            Form{
                TextField("First Name", text: $firstName)
                TextField("Last Name", text: $lastName)

            }
            .navigationTitle("Add new Player")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

                    let player = Player(firstName: firstName, lastName: lastName)
                    // How to append this to teamsList.teams.teamMember??
                    playersList.players.append(player)
                    dismiss()
                }

            }
        }
    }
}

struct PlayerDetailView: View {

    var player: Player?

    var body: some View {
        if player != nil {

                Text("Hello \(player!.firstName) \(player!.lastName)")

        }
        else {
            VStack {
                Image(systemName: "person.fill")
                Text("Select Player")
            }
        }
    }
}

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

1      

@SwiftiJoe explains his pain:

Actually I am struggling over the "ForEach"

I like to think of ForEach as a view factory!
See -> View Factory
or See -> View Factory
or See -> View Factory

So, ask yourself: What is my ForEach view factory trying to make?

It appears you're trying to feed the numbers (0..<3) into your view factory? So how can you create a player out of the integers 0, 1, and 2? This seems like a hopeless task.

// ForEach is a view factory. It makes other views. 
// You are feeding the integers 0, 1, and 2 INTO this factory
ForEach(0..<3) { player in
    NavigationLink("Hello \(player)", value: player)
}

Instead, you defined a team to have a collection of player objects.

struct Team: Identifiable, Codable, Hashable {
    var id = UUID()
    var name: String
    var teamMembers: [Player]  // <--- Here! This is a collection!
}

Instead of feeding integers to your ForEach view factory, give it the raw materials it needs to build player views!

ForEach( selectedTeam.teamMembers, id:\.self) { onePlayer in
    TeamMemberView( for: onePlayer )  // <-- Create a new view struct to display one and only one player.
}

Keep Coding!

Come back and let us know how you solved your logic problem

1      

Thanks for your reply and sorry for the misunderstanding. The ForEach over the numbers was just a dummy. Of course I want to go and see one specific player.

Trying your code like this:

ForEach( team.teamMembers, id:\.self) { onePlayer in
    TeamMemberView( for: onePlayer )  // <-- Create a new view struct to display one and only one player.
}

I get an errormessage: "Cannot find 'team' in scope". Of course (?) because it is in "teamsList.teams.teamMember". I think there is a simple step missing to solve this...but I don`t get it.

1      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

In my example, I used team.teamMembers.

In your actual code you defined selectedTeam.

ForEach( selectedTeam.teamMembers ) { onePlayer in
    TeamMemberView( for: onePlayer )  // <-- Create a new view struct to display one and only one player.
}

Because your Player object conforms to Identifiable protocol, you may omit the id:\.self boilerplate.

Extra Credit Reading Assignment:

See -> Renaming ContentView

Please read it, then leave a comment!

1      

That made my day. Trying to use the ForEach with my StateObject "teamsList" instead of the "selectedTeam" makes definitely more sense... My first question is solved.

My second question was about the "Add"-Button in the PlayerAddView.

struct PlayerAddView: View {

    @Environment(\.dismiss) var dismiss

    @State private var firstName = ""
    @State private var lastName = ""

    var body: some View {
        NavigationStack {
            Form{
                TextField("First Name", text: $firstName)
                TextField("Last Name", text: $lastName)

            }
            .navigationTitle("Add new Player")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

                    let player = Player(firstName: firstName, lastName: lastName)
                    playersList.players.append(player)
                    dismiss()
                }

            }
        }
    }
}

I added the follwoing to my PlayerAddView:

@Binding var selectedTeam: Team?

In my ContentView I added the Binding to my .sheet in the toolbar:

 .sheet(isPresented: $showingAddPlayer) {
                        PlayerAddView(selectedTeam: $selectedTeam)
                    }

The Button in the toolbar of my PlayerAddView now looks like:

                Button("Add") {

                    let player = Player(firstName: firstName, lastName: lastName)
                    selectedTeam!.players.append(player)
                    dismiss()
                }

But now I have a strange behavior. I can add and delete Teams. I can add Players to the Teams and the PlayerAddView recognizes the selectedTeam. But after pushing the Add-Button in my PlayerAddView, the player shows up for a second and then disappears?! Where did it go?

For review the complete code:

import SwiftUI

struct Team: Identifiable, Codable, Hashable {
    var id = UUID()
    var name: String
    var players: [Player]
}

struct Player: Identifiable, Codable, Hashable {
    var id = UUID()
    var firstName: String
    var lastName: String
}

class TeamsList: ObservableObject {
    @Published var teams: [Team] = []

    @MainActor class TeamsList: ObservableObject {
        @Published var teams = [Team]() {
            didSet {
                if let encoded = try? JSONEncoder().encode(teams) {
                    UserDefaults.standard.set(encoded, forKey: "Teams")
                }
            }
        }

        init() {
            if let savedItems = UserDefaults.standard.data(forKey: "Teams"){
                if let decodedItems = try? JSONDecoder().decode([Team].self, from: savedItems) {
                    teams = decodedItems
                    return
                }
            }
            teams = []
        }
    }
}

struct TeamAddView: View {

    @EnvironmentObject var teamsList: TeamsList

    @Environment(\.dismiss) var dismiss

    @State private var name = ""

    var body: some View {
        NavigationStack {
            Form{
                TextField("Team Name", text: $name)

            }
            .navigationTitle("Add new Team")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

                    let team = Team(name: name, players: [])
                    teamsList.teams.append(team)
                    dismiss()
                }
            }
        }
    }
}

struct PlayerAddView: View {

    @Environment(\.dismiss) var dismiss

    @State private var firstName = ""
    @State private var lastName = ""

    @Binding var selectedTeam: Team?

    var body: some View {
        NavigationStack {
            Form{
                TextField("First Name", text: $firstName)
                TextField("Last Name", text: $lastName)

            }
            .navigationTitle("Add new Player to \(selectedTeam!.name)")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

                    let player = Player(firstName: firstName, lastName: lastName)
                    selectedTeam!.players.append(player)
                    dismiss()
                }

            }
        }
    }
}

struct PlayerDetailView: View {

    var player: Player?

    var body: some View {
        if player != nil {

                Text("Hello \(player!.firstName) \(player!.lastName)")

        }
        else {
            VStack {
                Image(systemName: "person.fill")
                Text("Select Player")
            }
        }
    }
}

struct ContentView: View {

    @StateObject var teamsList = TeamsList()

    @State private var showingAddPlayer = false
    @State private var showingAddTeam = false

    @State private var selectedTeam: Team?
    @State private var selectedPlayer: Player?

    var body: some View {
        NavigationSplitView {
            List(selection: $selectedTeam){

                ForEach(teamsList.teams) { team in
                    NavigationLink(team.name, value: team)
                }
                .onDelete(perform: removeTeam)
            }
            .navigationTitle("Teams")
            .toolbar{
                EditButton()

                Button{
                    showingAddTeam = true
                } label: {
                    Image(systemName: "person.badge.plus")
                }

                .sheet(isPresented: $showingAddTeam) {
                    TeamAddView()
                }

            }
        } content: {
            if selectedTeam != nil {
                List(selection: $selectedPlayer){

                    ForEach(selectedTeam!.players) { player in
                        NavigationLink(player.firstName, value: player)
                    }
                }
                .navigationTitle(selectedTeam?.name ?? "Player")

                .toolbar{
                    EditButton()

                    Button{
                        showingAddPlayer = true
                    } label: {
                        Image(systemName: "person.badge.plus")
                    }

                    .sheet(isPresented: $showingAddPlayer) {
                        PlayerAddView(selectedTeam: $selectedTeam)
                    }

                }
            } else {
                VStack {
                    Image(systemName: "person.3.fill")
                    Text("Select a Team")
                }
            }

        } detail: {
            PlayerDetailView(player: selectedPlayer)
        }
        .environmentObject(teamsList)
    }

    func removeTeam(at offsets: IndexSet) {
        teamsList.teams.remove(atOffsets: offsets)
    }
}

1      

@Joe continues his learning adventures!

My second question was about the "Add"-Button in the PlayerAddView.

Ok, am working an answer. Perhaps, in the meantime, you'll reciprocate and write a few sentences here about the four links I suggested?

1      

@Obelix asked to leave a comment on the ViewFactory-Concept

In one of your links to the ViewFactory-Concept you recomment to follow 3 questions:

What is the collection of things is this processing? What makes each thing unique? What is the ForEach factory making?

In my opinion – as an absolute beginner – the concept of a ViewFactory may give a helpful picture in my mind on what the ForEach is going to do for me. Following your 3 questions I maybe would have solved my first question on my own. One thing that I always have to think about twice are the TYPEs. What am I giving in and what do I want to get back. Because otherwise I get error messages like

“error on this line: Cannot convert value of type 'XYZ' to expected argument type 'ABC'

Or something else.

@Obelix asked about an opinion on renaming the ContentView

I really like your comment

“To me, keeping a view named ContentView is similar giving a variable the name holdStuff or boxOfSomething. It's like naming a function "task1( input1: input2:) -> Output"

This is definitely not the way we should handle naming of variables, functions or files. For me – following many tutorials – it helps to see/find the entry into an app easily as long as the entry is “ContentView”.

But for my own apps I will consider the renaming of the ContentView to a more logical name. Using the “refactor”-function in Xcode it should be an easy step to do.

1      

I already found 1 mistake in my code. My TeamsList-class started like this:

class TeamsList: ObservableObject {
    @Published var teams: [Team] = []

    @MainActor class TeamsList: ObservableObject {
        @Published var teams = [Team]() {...

I changed it to:

@MainActor class TeamsList: ObservableObject {
        @Published var teams = [Team]() {...

Now the teams get stored as expected. But the players added to the teams still disappear each time the playerListView is reloaded! Why?

1      

After some more testing, everything seems to be fine. In can add teams and I can add players. Crosschecking with some print-functions to see whats actually in my teams-list and the players-array, the team seems to hold my players in their players-array. But after refreshing the view (e.g. tapping an other team), all players in the first tapped team disapper.

My opinion: the Add-Button in the PlayerAddView is wrong? Some suggestions?

struct PlayerAddView: View {

    @Environment(\.dismiss) var dismiss

    //brauche ich das hier?
    @EnvironmentObject var teamsList: TeamsList

    @State private var firstName = ""
    @State private var lastName = ""

    @Binding var selectedTeam: Team?

    var body: some View {
        NavigationStack {
            Form{
                TextField("First Name", text: $firstName)
                TextField("Last Name", text: $lastName)

            }
            .navigationTitle("Add new Player to \(selectedTeam!.name)")
            .toolbar{

                Button("Cancel") {
                    dismiss()
                }
                Button("Add") {

//Something seems to go wrong here
                    let player = Player(firstName: firstName, lastName: lastName)
                    selectedTeam!.players.append(player)
                    dismiss()
                }

            }
        }
    }
}

1      

Finally I changed the Code of my Add-Button to:

                Button("Add") {

                    let player = Player(firstName: firstName, lastName: lastName)
                    selectedTeam!.players.append(player)
                    if let index = teamsList.teams.firstIndex(where: {$0.id == selectedTeam!.id}) {
                        teamsList.teams[index] = selectedTeam!
                    }

                    dismiss()
                }

In addition I changed the removePlayer-function to:

func removePlayer(at offsets: IndexSet) {
        selectedTeam!.players.remove(atOffsets: offsets)
        if let index = teamsList.teams.firstIndex(where: {$0.id == selectedTeam!.id}) {
            teamsList.teams[index] = selectedTeam!
        }
    }

Now everything works as expected. But I am not sure, if this is good coding, or if there is an other and better option. If there is, please let me know.

1      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.