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