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) only the content of the first list item is shown in the displayed form
- b) clicking cancel in the sheet, the sheet will appear for every item in the list before disappearing, containing the respective values.
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.
First image as example of upper question
Second image as example of upper question
CodePudding user response:
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.
Option 1 - 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)
)
}
Option 2 - individual subviews
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.