swiftuibindingswift-data

Bind to property on first element in SwiftData @Query array


I have a SwiftData model Player and there should only ever be 0 or 1 in the database. When the user is on the Main Menu and taps to Start a New Game, I walk them through a couple of screens to pick the options for the game. Once all of the options are chosen, I delete all existing Player objects and insert a new one. Then I dismiss all of those options picking screens.

Then what I want to happen is to automatically show the game view. But I'm not sure how to bind my .fullScreenCover() to the Player.isShown property on the first element in my SwiftData @Query.

The code below doesn't work because: Cannot convert value of type 'Bool?' to expected argument type 'Binding<Bool>'

I'm still very new to Swift and SwiftUI, so my grasp on bindables and observables is still tenuous.

struct RootContentView: View {
    @Query var activeplayers: [Player] // should only ever be 0 or 1

    var body: some View {
        ZStack {
            // stuff            
        }
        .fullScreenCover(isPresented: activeplayers.first?.isshown content: {
                GameView()
        })
        
    }
    
}

How can I automatically show my GameView screen based on the optional first element in my SwiftData @Query?


I also tried just querying to see if any players exist with .isShown == true and then binding to $activeplayers.isEmpty (this is the reverse of the logic I actually want, but I was just trying to find SOME way to distill the data down to a single bool property and I figured I could work on reversing the logic later) but that didn't work either:

@Query (filter: #Predicate<Player> {
        $0.isshown == true
    }) var activeplayers: [Player]

// ...

// without the $, gives the error: 
// Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
.fullScreenCover(isPresented: activeplayers.isEmpty content: {
        GameView()
})

// with the $, gives the error:
// Cannot find '$activeplayers' in scope
.fullScreenCover(isPresented: $activeplayers.isEmpty content: {
        GameView()
})

Solution

  • You could try this simple approach, using an extra @State var shouldShow and setting it in .onAppear {...} as shown in the example code

    struct RootContentView: View {
        @Environment(\.modelContext) private var modelContext
        @Query var activeplayers: [Player]
        
        @State var shouldShow = false // <-- here
        
        var body: some View {
            VStack {
                Text("testing")
            }
            .fullScreenCover(isPresented: $shouldShow) {  // <-- here
                GameView()
            }
            // -- here
            .onAppear {
                if let player = activeplayers.first, let doShow = player.isshown {
                    shouldShow = doShow
                }
            }
        }
    }