I have another project with almost identical setup without this problem, so I'm scratching my head as to why in this project, whenever a sheet is presented, the content code is being repeatedly called
struct TransactionsView: View {
@Environment(\.modelContext) private var modelContext
let sheet: Sheet
@Query private var transactions: [Transaction]
@State private var showCreate = false
init(sheet: Sheet) {
// init code to query for transactions matching Sheet ID
}
var body: some View {
List {
ForEach(transactions) { tx in
// Standard row stuff
}
.onDelete(perform: deleteTxs)
}
.toolbar {
ToolbarItem {
Button(action: addTransaction) {
Label("Add", systemImage: "plus")
}
}
}
.sheet(isPresented: $showCreate) {
TransactionCreateView(sheet: self.sheet)
.presentationDetents([.medium, .large])
}
}
private func addTransaction() {
withAnimation {
showCreate.toggle()
}
}
private func deleteTxs(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(transactions[index])
}
}
}
}
The sheet view
struct TransactionCreateView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.modelContext) private var modelContext
@State private var transaction: Transaction
@State private var notesText: String = ""
let sheet: Sheet
init(sheet: Sheet) {
self.sheet = sheet
self.transaction = Transaction(sheet: self.sheet)
}
var body: some View {
Form {
Picker("Type", selection: $transaction.type) {
ForEach(TransactionType.allCases, id: \.self) { type in
Text(type.rawValue.capitalized)
.tag(type)
}
}
.pickerStyle(.segmented)
TextField("Amount", value: $transaction.value, format: .number)
TextField("Notes", text: $notesText, prompt: Text("What was this for?"))
Button("Add") {
withAnimation {
transaction.notes = notesText.isEmpty ? nil : notesText
modelContext.insert(transaction)
}
dismiss()
}
}
.onAppear {
notesText = transaction.notes ?? ""
}
}
}
Model files
@Model
final class Transaction {
var timestamp: Date
var value: Double
var type: TransactionType
var notes: String?
@Relationship(inverse: \Sheet.transactions) var sheet: Sheet?
}
enum TransactionType: String, Codable, CaseIterable {
case income
case expense
}
@Model
final class Sheet {
var timestamp: Date
var title: String
var transactions: [Transaction]
}
I have narrowed down the issue to TransactionCreateView(sheet: self.sheet)
being called repeatedly (verified via breakpoint), but I'm not sure why. I'm guessing that somehow somewhere is causing something to be redrawn repeatedly. My other project also has .sheet() attached in this similar fashion yet doesn't experience this issue.
The reason that the init get's called in an infinite loop is that in the init of the view you creates a new Transaction
object and sets the relationship property sheet
. Now since the passed Sheet
object is already persisted (inserted in the ModelContext) then SwiftData will automatically insert the new Transaction
object into the ModelContext instance (because otherwise the data would be inconsistent if only one end of the relationship existed in the context).
This insert of the new transaction will trigger the @Query
in the parent view and a redraw in which the child view gets called again and you have an infinite loop.
Furthermore it's worth noting that since the transaction is always inserted there is no way for the user to regret adding a transaction by pressing escape when the sheet is open.
The simple solution is to not assign the sheet when creating the transaction and instead doing that in the action for the "Add" button.
init(sheet: Sheet) {
self.sheet = sheet
transaction = Transaction(sheet: nil)
}
But maybe a better solution since you don't have so many properties is to instead create local @State
properties and create and insert the Transaction
in the "Add" button action
@State private var type: TransactionType = .income
@State private var value: Double = .zero
@State private var notesText: String = ""
let sheet: Sheet
var body: some View {
Form {
Picker("Type", selection: $type) {
ForEach(TransactionType.allCases, id: \.self) { type in
Text(type.rawValue.capitalized)
.tag(type)
}
}
.pickerStyle(.segmented)
TextField("Amount", value: $value, format: .number)
TextField("Notes", text: $notesText, prompt: Text("What was this for?"))
Button("Add") {
withAnimation {
let transaction = Transaction(timestamp: .now,
value: value,
type: type,
notes: notesText.isEmpty ? nil : notesText,
sheet: self.sheet)
modelContext.insert(transaction)
}
dismiss()
}
}
}