Jordan Niedzielski
Jordan Niedzielski

Reputation: 123

Why does this SwiftUI alert show so eagerly?

The alert shows when it shouldn't

I'm incorporating Apple's HIG to my app. As such, I want to show the user an alert when an important piece of data is about to be deleted.

MRE - your pals

In this sample app user manages one's friendships. While cleaning one's social circle, user might accidentally delete a cool Dude. When this happens, i.e. when there is at least one Dude with isCool == true, the app shall show an alert. If that's not the case, it should just delete all the dudes denoted in selectedDudesIDs.

The problem

Currently, the app achieves its main goal - does not allow to delete a cool Dude without confirmation. However, for some reason, an empty alert is being shown to the user when not cool Dudes are selected.

MRE code

That's the code for a Swift Playground that illustrates the issue, "batteries included".

import SwiftUI
import PlaygroundSupport

struct TestView: View {
    @State private var selectedDudesIDs = Set<Dude.ID>()
    @State private var editMode: EditMode = .inactive
    @State private var dudes: [Dude] = [Dude(name: "John", isCool: false), Dude(name: "Paul", isCool: true)]
    // MARK: deleting alert
    /// A boolean flag initiating deletion.
    ///
    /// Its property observer determines whether there are any cool dudes, i.d. `dude.isCool == true` among the ones to be deleted.
    @State private var isDeletingDudes: Bool = false {
        willSet {
            let coolDudesAmongDeleted = dudes.filter { dude in
                selectedDudesIDs.contains(dude.id)
            }.filter { dude in
                dude.isCool
            }
            if !coolDudesAmongDeleted.isEmpty {
                coolDudesToBeDeletedCount = coolDudesAmongDeleted.count
            }
        }
    }
    @State private var coolDudesToBeDeletedCount: Int?
    
    /// Removes dudes with selected IDs from your `dudes`.
    func endFriendship() {
        dudes = dudes.filter { dude in
            !selectedDudesIDs.contains(dude.id)
        }
    }
    
    var body: some View {
        VStack {
            HStack {
                EditButton()
                Spacer()
            }
            Text("Your pals")
                .font(.title)
            List(dudes, selection: $selectedDudesIDs) { dude in
                Text(dude.name)
            }
            if editMode.isEditing && !selectedDudesIDs.isEmpty {
                Button(role: .destructive) {
                    isDeletingDudes = true
                } label: {
                    Text("They ain't my pals no more")
                }
            }
        }
        .environment(\.editMode, $editMode)
        // gimme some proportions
        .padding()
        .frame(minWidth: 500*0.9, minHeight: 500*1.6)
        .alert("End Friendship", isPresented: $isDeletingDudes, presenting: coolDudesToBeDeletedCount) { count in
            Button(role: .destructive) {
                endFriendship()
                coolDudesToBeDeletedCount = nil
            } label: {
                Text("End Friendships")
            }
            Button(role: .cancel) {
                coolDudesToBeDeletedCount = nil
            } label: {
                Text("Cancel")
            }
        } message: { _ in
            Text("You're are about to end friendship with at least one cool dude.")
        }
    }
}

struct Dude: Identifiable {
    var id: String { self.name }
    let name: String
    var isCool: Bool
}

let view = TestView()
PlaygroundPage.current.setLiveView(view)

MRE in action

A 27 seconds long clip illustrating the current behavior.

enter image description here

Here, the user knows two Dudes - John is just a friend and Paul is a cool friend. The alert should not be shown when deleting John.

Why do I think it's not my fault

The documentation of alert property wrapper reads: For the alert to appear, both isPresented must be true and data must not be nil. In this case, the alert is shown despite data (coolDudesToBeDeletedCount in this case) being nil. I've inspected it using a property observer on this variable and it's nil until one actually selects a cool Dude.
Also, the data parameter is of type T?, which is a generic Optional - and Int? definitely fits the role.

Wrap up

Is there a fault in my program's design or are the docs wrong? Either way, how could I achieve the result I'm after? How to show the alert only when it's necessary?

Upvotes: 1

Views: 233

Answers (1)

khawar ali
khawar ali

Reputation: 339

Yeah, it looks like the doc is misleading. only isPresented controls the visibility of the alert and presenting is to call the closure. if presenting is nil then the closure code will not execute.

workaround: create a var showAlert to control the visibility of the alert.


import SwiftUI

struct Dude: Identifiable {
    var id: String { self.name }
    let name: String
    var isCool: Bool
}

struct ContentView: View {
    @State private var selectedDudesIDs = Set<Dude.ID>()
    @State private var editMode: EditMode = .inactive
    @State private var dudes: [Dude] = [Dude(name: "John", isCool: false), Dude(name: "Paul", isCool: true)]
    // MARK: deleting alert
    /// A boolean flag initiating deletion.
    ///
    /// Its property observer determines whether there are any cool dudes, i.d. `dude.isCool == true` among the ones to be deleted.
    @State private var isDeletingDudes: Bool = false {
        willSet {
            let coolDudesAmongDeleted = dudes.filter { dude in
                selectedDudesIDs.contains(dude.id)
            }.filter { dude in
                dude.isCool
            }
            print(coolDudesAmongDeleted)
            if !coolDudesAmongDeleted.isEmpty {
                coolDudesToBeDeletedCount = coolDudesAmongDeleted.count
                showAlert = true
            }else{
                showAlert = false
                endFriendship()
            }
        }
    }
    @State private var showAlert: Bool = false
    @State private var coolDudesToBeDeletedCount: Int?
    
    /// Removes dudes with selected IDs from your `dudes`.
    func endFriendship() {
        dudes = dudes.filter { dude in
            !selectedDudesIDs.contains(dude.id)
        }
    }
    
    var body: some View {
        VStack {
            HStack {
                EditButton()
                    .padding(.leading, 24)
                Spacer()
            }
            Text("Your pals")
                .font(.title)
            List(dudes, selection: $selectedDudesIDs) { dude in
                Text(dude.name)
            }
            if editMode.isEditing && !selectedDudesIDs.isEmpty {
                Button(role: .destructive) {
                    isDeletingDudes = true
                } label: {
                    Text("They ain't my pals no more")
                }
            }
        }
        .environment(\.editMode, $editMode)
        // gimme some proportions
        .padding()
        .frame(minWidth: 500*0.9, minHeight: 500*1.6)
        .alert("End Friendship", isPresented: $showAlert, presenting: coolDudesToBeDeletedCount) { count in
            Button(role: .destructive) {
                endFriendship()
                coolDudesToBeDeletedCount = nil
            } label: {
                Text("End Friendships")
            }
            Button(role: .cancel) {
                coolDudesToBeDeletedCount = nil
            } label: {
                Text("Cancel")
            }
        } message: { _ in
            Text("You're are about to end friendship with at least one cool dude.")
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 1

Related Questions