swiftdataswift-data-relationship

Swift Data Query only showing partial information?


I have an issue with Swift Data persistence on a sample app I'm working on.

I have two @Model classes set up, one for bets:

class Bet {
    var id: UUID
    var betDescription: String
    var wagerAmount: Int
    var bettors: [Bettor] = []
    var winner: Bettor?
    var isPaidOut: Bool
    
    init(id: UUID = UUID(), betDescription: String, wagerAmount: Int, bettors: [Bettor] = [], winner: Bettor? = nil, isPaidOut: Bool = false) {
        self.id = id
        self.betDescription = betDescription
        self.wagerAmount = wagerAmount
        self.bettors = bettors
        self.winner = winner
        self.isPaidOut = isPaidOut
    }
}

and one for Bettors:

class Bettor {
    var id: UUID
    var name: String
    var bets: [Bet] = []
    
    var wins: Int {
        // Compute number of wins based on related bets
        bets.filter { $0.winner?.id == id }.count
    }
    
    var losses: Int {
        // Compute number of losses based on related bets
        bets.filter { $0.winner?.id != id && $0.winner != nil }.count
    }
    
    init(id: UUID = UUID(), name: String, bets: [Bet] = []) {
        self.id = id
        self.name = name
        self.bets = bets
    }
}

I have an AddBetView with a function to insert a new bet into the modelContext via an AddBet function:

import SwiftUI
import SwiftData

struct AddBetView: View {
    @Environment(\.modelContext) var modelContext
    @Environment(\.dismiss) private var dismiss

    @Query(sort: \Bettor.name) var bettors: [Bettor]
    
    @State private var selectedBettor1: Bettor?
    @State private var selectedBettor2: Bettor?
    @State private var betDescription: String = ""
    @State private var wagerAmount: String = ""
    
    var body: some View {
        VStack(alignment: .leading) {
            Form {
                Section(header: Text("Bet Details")) {
                    TextField("Bet Description", text: $betDescription)
                    TextField("Wager Amount", text: $wagerAmount)
                        .keyboardType(.numberPad)
                }
                
                Section(header: Text("Select Bettors")) {
                    Picker("Bettor 1", selection: $selectedBettor1) {
                        Text("Select Bettor 1").tag(Bettor?.none)
                        ForEach(bettors) { bettor in
                            Text(bettor.name).tag(bettor as Bettor?)
                        }
                    }
                    .pickerStyle(MenuPickerStyle())
                    
                    Picker("Bettor 2", selection: $selectedBettor2) {
                        Text("Select Bettor 2").tag(Bettor?.none)
                        ForEach(bettors) { bettor in
                            Text(bettor.name).tag(bettor as Bettor?)
                        }
                    }
                    .pickerStyle(MenuPickerStyle())
                }
                
                Button("Add Bet") {
                    addBet()
                }
                .disabled(betDescription.isEmpty || wagerAmount.isEmpty || selectedBettor1 == nil || selectedBettor2 == nil || selectedBettor1 == selectedBettor2)
            }
        }
        .padding()
    }
    
    private func addBet() {
        guard let wager = Int(wagerAmount), let bettor1 = selectedBettor1, let bettor2 = selectedBettor2 else {
            print("Error: Invalid input or selection.")
            return
        }
        
        // Ensure bettors are different
        guard bettor1.id != bettor2.id else {
            print("Error: Bettors cannot be the same.")
            return
        }
        
        let bet = Bet(betDescription: betDescription, wagerAmount: wager, bettors: [bettor1, bettor2])
        
        modelContext.insert(bet)
        
        // Debugging: Print current state before appending the bet
        print("Before appending:")
        print("Bettor 1 bets: \(bettor1.bets.map { $0.betDescription })")
        print("Bettor 2 bets: \(bettor2.bets.map { $0.betDescription })")
        
        
        // Append bet to bettors
        bettor1.bets.append(bet)
        bettor2.bets.append(bet)
        
  
        // Debugging: Print current state after appending the bet
        print("After appending:")
        print("Bettor 1 bets: \(bettor1.bets.map { $0.betDescription })")
        print("Bettor 2 bets: \(bettor2.bets.map { $0.betDescription })")
        
        print(bettors)
        do {
            try modelContext.save()
            print("Successfully saved the bet.")
        } catch {
            print("Failed to save bet: \(error)")
        }
        
        // Clear form
        betDescription = ""
        wagerAmount = ""
        selectedBettor1 = nil
        selectedBettor2 = nil
        
        dismiss()
        // dismiss the add bet sheet
        
    }
}

However, this doesn't seem to be correctly updating the bettors correctly. Only one of the bettors has this bet added, and it's always selectedBettor1. This is despite the debugging bit printing what seems to be correct:

Before appending:
Bettor 1 bets: []
Bettor 2 bets: []
After appending:
Bettor 1 bets: ["Sherri Wins"]
Bettor 2 bets: ["Sherri Wins"]

In a BettorDetailView I would like to show a list of each bettor's bets but only one bettor gets their bet appended correctly.

import SwiftData

struct BettorDetailView: View {
    var bettor: Bettor
    
    var body: some View {
        Form {
            Section(header: Text("Bettor Details")) {
                Text("Name: \(bettor.name)")
                    .font(.headline)
                
                VStack(alignment: .leading) {
                    Text("Wins vs. Losses")
                        .font(.headline)
                    
                    BurritoBarChartView(wins: Double(bettor.wins), losses: Double(bettor.losses))
                        .frame(height: 200) // Adjust the height as needed
                }
            }
            .onAppear {
                print("Bettor bets: \(bettor.bets.map { $0.betDescription })")

            }
            
            Section(header: Text("Bets")) {
                List(bettor.bets) { bet in
                    NavigationLink(destination: BetDetailView(bet: bet)) {
                        Text(bet.betDescription)
                            .font(.body)
                    }
                }
            }
        }
        .navigationTitle("Bettor Details")
        .navigationBarTitleDisplayMode(.inline)
    }
}

I get a good debug printout of the "Sherri Wins" bet for bettor 1 but an empty array for bettor 2.

Is this an issue with my AddBet() function or am I incorrectly querying data? Is my relationship set up incorrectly?


Solution

  • The main problem here is that you are not using the @Relationship macro for your relationships and the fact that you have two references to Bettor in your Bet model so how can SwiftData correctly know which one to use?

    And there is also only one Bet property in in the Bettor model which is incorrect (unless you are running the latest beta).

    So make sure you use the macro with the inverse: parameter.

    Bet model:

    @Relationship(inverse: \Bettor.bets) 
    var bettors: [Bettor] = []
    
    @Relationship(inverse: \Bettor.wins)
    var winner: Bettor?
    

    And fix Bettor

    var bets: [Bet] = []
    var wins: [Bet] = [] // I removed the computed property with the same name
    

    I would also change the code for creating the bet and relationships to

    let bet = Bet(betDescription: betDescription, wagerAmount: wager)
    modelContext.insert(bet)
    
    bet.bettors.append(bettor1)
    bet.bettors.append(bettor2)