I'm not sure if this is a bug or if I'm doing something wrong, because I want to show an alert if my person has no item array. When I'm deleting the items, the alert is shown, but sometimes it shows the alert only for a second and then disappears. Sometimes the alert shows until I press a button but then the entire text is bold. I have startet the simulator many times and tested it but I could not find any correlation. My guess is, that the alert is activated or shown multiple times but I don't know where or why.
That's my code, the alert is in the "SectionRowView".
class Person: Identifiable, ObservableObject {
let id = UUID()
@Published var name: String
@Published var item: [TodolistItem]
init(name: String, item: [TodolistItem]) {
self.name = name
self.item = item
}
}
class TodolistItem: Identifiable, ObservableObject {
@Published var todoName: String
@Published var priority: String
init(todoName: String, priority: String) {
self.todoName = todoName
self.priority = priority
}
}
struct Todolist: View {
@State private var personen: [Person] = [
Person(name: "Michi", item: [
TodolistItem(todoName: "Reifenwechsel", priority: "Niedrig"),
TodolistItem(todoName: "Irgendwas", priority: "Mittel")
]),
Person(name: "Tina", item: [
TodolistItem(todoName: "Haushalt", priority: "Dringend!")
])
]
@State private var addSheet: Bool = false
let navTitle: String
let listInfo: ListInfo
var body: some View {
NavigationStack {
List {
ForEach(personen) { person in
SectionRowView(person: person)
}
}
.listStyle(.sidebar)
}
.navigationTitle(navTitle)
.navigationBarItems(trailing:
Button(action: { addSheet.toggle() }, label: {
Image(systemName: "plus.circle")
})
)
.sheet(isPresented: $addSheet) {
AddTodoSectionView(personen: $personen)
.presentationDetents([.fraction(0.4)])
}
.toolbarBackground(listInfo.backgroundColor.opacity(0.6), for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
}
}
struct SectionRowView: View {
@State private var isExpanded: Bool = true
@State private var editSheet: Bool = false
@State private var showAlert: Bool = false
@ObservedObject var person: Person
var body: some View {
Section(isExpanded: $isExpanded) {
ForEach(person.item) { item in
HStack {
TodoRowView(item: item)
Button { editSheet.toggle() } label: {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
.foregroundStyle(.primary)
}
}
.onDelete { offSet in
person.item.remove(atOffsets: offSet)
if person.item.isEmpty {
showAlert = true
}
}
.sheet(isPresented: $editSheet) {
EditView()
}
Button {
let newItem = TodolistItem(todoName: "Opfer", priority: "Niedrig")
withAnimation {
person.item.append(newItem)
}
} label: {
HStack {
Image(systemName: "plus")
Text("Neues Todo hinzufügen")
}
.foregroundStyle(.blue)
}
} header: {
Text(person.name)
}
.alert("Person löschen?", isPresented: $showAlert) {
Button("Behalten", role: .cancel) {
showAlert = false
}
Button("Löschen", role: .destructive) {
showAlert = false
}
} message: {
Text("Die Person hat alle Aufgaben erledigt, möchtest du das diese gelöscht wird?")
}
}
}
struct TodoRowView: View {
@State private var priority: [String] = [
"Niedrig", "Mittel", "Hoch", "Dringend!"
]
@State private var isDone: Bool = false
@State private var textFieldText: String = ""
@ObservedObject var item: TodolistItem
var body: some View {
VStack {
HStack {
Button(action: {
guard textFieldText != "" else { return }
withAnimation {
isDone.toggle()
}
}, label: {
Image(systemName: isDone ? "checkmark.circle.fill" : "checkmark.circle")
.foregroundStyle(isDone ? .green : .red)
})
TextField(item.todoName, text: $textFieldText, prompt: Text("Todo eintragen"))
.strikethrough(isDone ? true : false)
.foregroundStyle(isDone ? Color.gray.opacity(0.7) : Color.primary)
Spacer()
if item.priority == "Niedrig" {
Menu(item.priority) {
ForEach(priority, id: \.self) { prio in
Button(prio) {
item.priority = prio
}
}
}
.foregroundStyle(.green)
} else if item.priority == "Mittel" {
Menu(item.priority) {
ForEach(priority, id: \.self) { prio in
Button(prio) {
item.priority = prio
}
}
}
.foregroundStyle(.blue)
} else if item.priority == "Hoch" {
Menu(item.priority) {
ForEach(priority, id: \.self) { prio in
Button(prio) {
item.priority = prio
}
}
}
.foregroundStyle(.orange)
} else if item.priority == "Dringend!" {
Menu(item.priority) {
ForEach(priority, id: \.self) { prio in
Button(prio) {
item.priority = prio
}
}
}
.foregroundStyle(.red)
}
}
}
}
}
struct EditView: View {
var body: some View {
Text("Hi")
}
}
struct AddTodoSectionView: View {
@State private var sectionTextField: String = ""
@Binding var personen: [Person]
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
VStack {
TextField("", text: $sectionTextField, prompt: Text("Füge eine neue Person hinzu"))
.frame(width: 250, height: 50)
.padding(.horizontal)
.background(Color.gray.opacity(0.3).cornerRadius(10))
.padding(.horizontal)
Button("Save") { addItem() }
.frame(width: 250, height: 50)
.padding(.horizontal)
.background(Color.gray.opacity(0.3).cornerRadius(10))
.padding(.horizontal)
}
.navigationTitle("Neue Person")
}
}
func addItem() {
let newItem = Person(name: sectionTextField, item: [TodolistItem(todoName: "Test1", priority: "Niedrig")])
personen.append(newItem)
dismiss()
}
}
#Preview {
Todolist(navTitle: "Todoliste", listInfo: ListInfo(listName: "", backgroundColor: .blue, accentColor: .white))
}
When an item is deleted from a person's item list, this causes a refresh of SectionRowView
. This is because, this view is observing changes to person
. If the flag showAlert
is set at the same time (which happens when the last item is deleted) then I guess there is a race condition between refreshing the view and showing the alert. Sometimes, the change of flag is lost during the refresh.
I found that one way to fix is to defer setting the flag by performing it asynchronously after a small delay (0.5s seems to be enough). But I don't think this is a good solution.
Looking at the bigger picture, how were you proposing to implement the button action that deletes the person, if this is performed inside SectionRowView
? This view does not have a handle to the array of persons. So:
👉 I would suggest moving the alert to the parent view Todolist
.
personen
.SectionRowView
.These changes get it working this way:
// Todolist
@State private var personIdWithoutItems: UUID?
@State private var showAlert: Bool = false
// ...
List {
// ...
}
.listStyle(.sidebar)
.onChange(of: personIdWithoutItems) { oldVal, newVal in
if newVal != nil {
showAlert = true
}
}
.alert("Person löschen?", isPresented: $showAlert) {
Button("Behalten", role: .cancel) {
personIdWithoutItems = nil
}
Button("Löschen", role: .destructive) {
personIdWithoutItems = nil
}
} message: {
Text("Die Person hat alle Aufgaben erledigt, möchtest du das diese gelöscht wird?")
}
// SectionRowView
// @State private var showAlert: Bool = false // -> delete
@Binding var personIdWithoutItems: UUID?
// ...
ForEach(person.item) { item in
// ...
}
.onDelete { offSet in
person.item.remove(atOffsets: offSet)
if person.item.isEmpty {
personIdWithoutItems = person.id
}
}
// .alert("Person löschen?", isPresented: $showAlert) { // -> delete