swiftdata - Create state array from query results in SwiftUI view using results of a Swift data query - Stack Overflow

时间: 2025-01-06 admin 业界

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:

  • View presents a list of players, each with an instance of the stopwatch
  • Swipe actions can start/stop the stopwatch for individual players
  • Edit mode can be used for starting the stopwatch for multiple players
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.

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:

  • View presents a list of players, each with an instance of the stopwatch
  • Swipe actions can start/stop the stopwatch for individual players
  • Edit mode can be used for starting the stopwatch for multiple players
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.

Share Improve this question edited 18 hours ago Lymedo asked 22 hours ago LymedoLymedo 60810 silver badges23 bronze badges 5
  • 1 Why have both GamePlayer and Player? Why not add the stopwatch (or whatever else you need) to the SwiftData model? They can be @Transient if you SwiftData to ignore them. – Sweeper Commented 21 hours ago
  • I’ll give that a go. I wasn’t away you could add an observable object to a swift data model. – Lymedo Commented 21 hours ago
  • @Sweeper so @Transient makes the stopwatch object available via the model, however, the property needs a default value i.e. the stopwatch object. I need to persist the time elapsed which I can do with an additional property in my model and I'll need to initialise the stopwatch object with this value for new app launches. ``` var gameTime: Double @Transient var timer: Stopwatch = Stopwatch(timeElapsed: gameTime) ``` But obviously I get "Cannot use instance member 'gameTime' within property initializer; property initializers run before 'self' is available". Any advice? – Lymedo Commented 18 hours ago
  • There is no gameTime in the code in the question. Please edit your question to clarify what your real code looks like. – Sweeper Commented 18 hours ago
  • @sweeper updated the example model code. This works but everytime I relaunch the app the stopwatch returns to 0. I'm updating the gameTime value on the trailing swipe action. – Lymedo Commented 18 hours ago
Add a comment  | 

1 Answer 1

Reset to default 1

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) }
}