Search code examples
swiftuialert

Alert is being dismissed immediately after being presented


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

Solution

  • 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.

    • The alert is triggered when a child signals that the list of items is empty.
    • This requires passing a binding to the child.
    • If the user confirms that the person should be deleted, the parent view can update the array personen.
    • This approach avoids the (apparent) race condition in the child view 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