swiftuiswiftdata

Create state array from query results in SwiftUI view using results of a Swift data query


I have the following view which works as I want with an array of objects and now I want to refactor it so that the array of objects is created by iterating over the results of a Swift data query.

The reason for this is because I need to access the functions in the observable stopwatch object from the Edit view. I could create the Player objects in the ForEach by passing the model and stopwatch objects to a child view, however, I am then unable to access the required functions in stopwatch object from the parent view when in Edit mode.

To summarise functionality:

struct ContentView: View {

    @State private var editMode = EditMode.inactive
    @State private var selectedPlayers: Set<Player.ID> = []

    @State var players = [
        Player(name: "Player 1", stopwatch: Stopwatch(timeElapsed: 0)),
        Player(name: "Player 2", stopwatch: Stopwatch(timeElapsed: 0)),
        Player(name: "Player 3", stopwatch: Stopwatch(timeElapsed: 0)),
    ]

    var body: some View {

        NavigationStack {

            VStack {

                List(selection: $selectedPlayers) {

                    ForEach(players) { player in

                        HStack {
                            Text(player.name)
                            Spacer()
                            StopwatchView(stopwatch: player.stopwatch)
                                .swipeActions(edge: .leading) {
                                    Button(action: {
                                        player.stopwatch.start()
                                    }) {
                                        Image(systemName: "play.circle.fill")
                                    }
                                }
                                .tint(.green)
                                .swipeActions(edge: .trailing) {
                                    Button(action: {
                                        player.stopwatch.stop()
                                    }) {
                                        Image(systemName: "stop.circle.fill")
                                    }
                                }
                                .tint(.red)
                        }

                    }

                }

            }
            .navigationTitle("Players")
            .toolbar {
                if editMode.isEditing == true && !selectedPlayers.isEmpty {
                    Button(
                        action: {

                            selectedPlayers.forEach { playerId in

                                let playerInstance = players.filter {
                                    $0.id == playerId
                                }
                                playerInstance.first?.stopwatch.start()

                            }

                        }) {
                            Image(systemName: "play.circle.fill")
                        }
                }
                EditButton()
            }
            .environment(\.editMode, $editMode)

        }

    }

}

Example model:

@Model
class GamePlayer: Identifiable {
    var id: String
    var name: String
    var gameTime: Double
    @Transient var timer: Stopwatch = Stopwatch()
    
    init(id: String = UUID().uuidString, name: String, gameTime: Double = 0) {
        self.id = id
        self.name = name
        self.gameTime = gameTime
    }
}

extension GamePlayer {
    static var defaults: [GamePlayer] {
        [
            GamePlayer(name: "Jake"),
            GamePlayer(name: "Jen"),
            GamePlayer(name: "Ben"),
            GamePlayer(name: "Sam"),
            GamePlayer(name: "Tim"),
        ]
    }
}

In content view, I need to generate an array of Player objects for each object in the GamePlayer model, with a stopwatch.

@Environment(\.modelContext) var modelContext
@Query() var gamePlayer: [GamePlayer]

So my question is, how do I iterate over the query results to create the required array of Player objects?

It would also be good to know if I'm approaching this in the correct way. I'm only a couple of months into learning Swift so please go easy on my code.


Solution

  • Make the players property into a @Query property instead

    @Query private var players: [GamePlayer]
    

    and then update the Stopwatch for each player in onAppear

    .onAppear {
        players.forEach { $0.stopwatch.gameTime = $0.gameTime }
    }
    

    You could also consider making the stopwatch property optional and assign a new instance in onAppear instead if it doesn't make sense for a GamePlayer object to always have one assigned.

    .onAppear {
        players.forEach { $0.stopwatch = Stopwatch(gameTime: $0.gameTime) }
    }