Short description: in detailView I've got a List of a related entity. For each item, there's a button to open the edit sheet for this item.
List {
if (book.booksBorrowers != nil) {
ForEach (Array(book.booksBorrowers! as! Set<Borrowers>), id: \.self) { borrower in
HStack {
Text(borrower.firstName ?? "unbekannter Vorname")
Text(borrower.lastName ?? "unbekannter Nachname")
Text(String(format: "%.0f", borrower.age))
Spacer()
Button {
showingBorrowerEditScreen.toggle()
} label: {
Image(systemName: "pencil")
.frame(width: 20.0, height: 20.0)
}.multilineTextAlignment(.center).buttonStyle(.borderless)
.sheet(isPresented: $showingBorrowerEditScreen) {
EditBorrowerView(
aBorrower: borrower,
firstName: borrower.firstName!,
lastName: borrower.lastName!,
age: Int(borrower.age)
).environment(\.managedObjectContext, self.viewContext)
}
}
}.onDelete(perform: deleteBorrower)
}
}.listStyle(.inset(alternatesRowBackgrounds: true))
On clicking one of the edit button within the list, a sheet appears with an edit form, prefilled with the values of the selected list item.
struct EditBorrowerView: View {
@Environment(\.managedObjectContext) var moc
@Environment(\.dismiss) var dismiss
@State private var firstName = ""
@State private var lastName = ""
@State private var age = 0.0
@StateObject var aBorrower: Borrowers
init(aBorrower: Borrowers, firstName: String, lastName: String, age: Int) {
self._aBorrower = StateObject(wrappedValue: aBorrower)
self._firstName = State(initialValue: aBorrower.firstName ?? "")
self._lastName = State(initialValue: aBorrower.lastName ?? "")
self._age = State(initialValue: Double(aBorrower.age))
}
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 0
return formatter
}()
var body: some View {
VStack {
Text("Ausleiher bearbeiten").font(.title)
Form {
VStack {
TextField("Vorname", text: $firstName)
TextField("Nachname", text: $lastName)
HStack {
Slider(value: $age, in: 0...99, step: 1)
TextField("Alter", value: $age, formatter: formatter)
}
}
}
HStack {
Button("Save") {
// save the edited book
aBorrower.firstName = firstName
aBorrower.lastName = lastName
aBorrower.age = Double(age)
try? moc.save()
dismiss()
}
Button("Cancel") {
dismiss()
}
}
}.padding(10)
}
}
But now, when clicking the edit button of a row
A small debugging with print() shows, that on clicking the edit button the correct values are set first (e.g. I click on third item, values of third item are passed), but additionally, all list items are passed to the sheet, too.
If you have a .sheet(isPresented:)
modifier in a view that is repeated in a loop, but your boolean state variable is outside the loop, then that one state variable is used for multiple copies of the sheet. Sometimes that may manifest as a sheet showing with details different to the row you clicked on; other times you might see multiple sheets. It sounds like the side effects you're seeing are related to this.
There are a couple of ways you can get around this.
sheet(item:)
Replace your showingBorrowerEditScreen
with a borrowerToEdit
state variable that is an optional object, defaulting to nil:
@State private var borrowerToEdit: Borrower? = nil
In your button action, set this to the borrower of the current row in the loop:
Button {
borrowerToEdit = borrower
} label: {
// etc
And finally, use the item:
form of the sheet modifier outside the ForEach
loop. Note that this form takes a block with a reference to the borrower object concerned.
ForEach(...) { borrower in
// etc.
}
.sheet(item: $borrowerToEdit) { borrower in
EditBorrowerView(
aBorrower: borrower,
firstName: borrower.firstName!,
lastName: borrower.lastName!,
age: Int(borrower.age)
)
}
If you want to stick with booleans to represent whether a modal sheet should be used, you'll need to isolate that boolean to its own subview, by extracting the row details. For example:
List {
if (book.booksBorrowers != nil) {
ForEach (Array(book.booksBorrowers! as! Set<Borrowers>), id: \.self) { borrower in
BorrowerListRow(borrower: borrower)
.onDelete(perform: deleteBorrower)
}
}
}
struct BorrowerListRow: View {
@ObservedObject var borrower: Borrower
@State private var showingBorrowerEditScreen = false
var body: some View {
HStack {
Text(borrower.firstName ?? "unbekannter Vorname")
Text(borrower.lastName ?? "unbekannter Nachname")
Text(String(format: "%.0f", borrower.age))
Spacer()
Button {
showingBorrowerEditScreen.toggle()
} label: {
Image(systemName: "pencil")
.frame(width: 20.0, height: 20.0)
}.multilineTextAlignment(.center).buttonStyle(.borderless)
.sheet(isPresented: $showingBorrowerEditScreen) {
EditBorrowerView(
aBorrower: borrower,
firstName: borrower.firstName!,
lastName: borrower.lastName!,
age: Int(borrower.age)
).environment(\.managedObjectContext, self.viewContext)
}
}
}
}
That way, each row has its own "should I be showing a modal" Boolean, so there can be no confusion about which row "owns" the active sheet, and only one will be able to be shown at a time.
Which option you go for is partly down to personal preference. I tend to favour option 1 in my own code, as it feels more that the modal is "owned" by the list rather than a row, and it reinforces the idea that you can only edit one borrower at a time. But either approach should eliminate the problems you're currently seeing.