0

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.

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. Commented Jan 4 at 19:42
  • I’ll give that a go. I wasn’t away you could add an observable object to a swift data model. Commented Jan 4 at 20:08
  • @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? Commented Jan 4 at 22:31
  • There is no gameTime in the code in the question. Please edit your question to clarify what your real code looks like. Commented Jan 4 at 22:49
  • @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. Commented Jan 4 at 23:09

1 Answer 1

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) }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for this. I was thinking about if it could be achieved with onAppear() earlier. I’ll give your suggestions a go.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.