Reputation: 123
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.
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
.
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 Dude
s are selected.
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)
A 27 seconds long clip illustrating the current behavior.
Here, the user knows two Dude
s - John
is just a friend and Paul
is a cool friend. The alert should not be shown when deleting John
.
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.
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
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