I have a problem in my SwiftUI project, and I am trying to use alert dialog to delete an item in a list, but when use the .alert, wrong item gets deleted. I am still do not know what I missed?
Here is my code:
struct CustomView: View {
#State private var selectedUsers: CustomModel?
#State var users: [CustomModel]
#State private var selectDelete = false
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(users){ user in
CustomRowView(user: user)
.contextMenu {
Button(action: {
self.delete(item: data)
}) {
Text("remove")
}
}
.onTapGesture {
selectedUsers = user
}
.alert(isPresented: $selectDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: user)
},
secondaryButton: .cancel()
)
}
.onDelete { (indexSet) in
self.users.remove(atOffsets: indexSet)
}
}
})
})
}
private func delete(item user: CustomModel) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users.remove(at: index)
}
}
}
model:
struct CustomModel: Identifiable{
var id = UUID().uuidString
var name: String
}
var users = [
CustomModel(name: "david"),
CustomModel(name: "marry"),
CustomModel(name: "henry"),
CustomModel(name: "nadi"), ]
There are 2 problems in your code that are creating the unexpected behaviour:
When you iterate through the user with ForEach, you are attaching one .alert() modifier to each single instance. This means, when selectDelete is set to true, all of the instances try to show an alert, but only one will. Which one? Who knows, but that instance will be deleted.
You have a nice selectedUsers variable that changes when you tap on it. But you are deleting the user, not selectedUser.
How you can fix your code to work with .alert() in 3 steps:
if you don't need to perform any task with the tap gesture, just delete it and change the selectedUser in your context menu:
.contextMenu {
Button(action: {
selectedUsers = user // Change here then delete
self.delete(item: data)
}) {
Text("remove") }}
// Forget about it...
//.onTapGesture {
// selectedUsers = user
//}
attach your alert to the top-most level of your view (at the bottom):
ScrollView {
...
}
.alert(isPresented: $selectDelete) {
...
}
delete selectedUser, not user:
self.delete(item: selectedUser)
Related
I want to delete list items and when I delete list items, it will show confirmation dialog like .alert dialog. I have code below and if I want to remove list item .alert dialog is work, but if I try to remove all list items, .alert dialog not work, and I am not able to remove all items, I do not know where I missed? I guess most probably it is due to the I have two .alert dialog and they are conflicted, any idea?
struct CustomView: View {
#State private var selectedUsers: CustomModel?
#State var users: [CustomModel]
#State private var selectDelete = false
#State private var selectAllDelete = false
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(users){ user in
CustomRowView(user: user)
.contextMenu {
Button(action: {
selectDelete = true
}) {
Text("remove")
}
Button(action: {
selectAllDelete = true
}) {
Text("remove all")
}
}
.alert(isPresented: $selectDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel()
)
}
.alert(isPresented: $selectAllDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel()
)
}
.onDelete { (indexSet) in
self.users.remove(atOffsets: indexSet)
}
}
})
})
}
private func delete(item user: CustomModel) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users.remove(at: index)
}
}
}
model:
struct CustomModel: Identifiable{
var id = UUID().uuidString
var name: String
}
var users = [
CustomModel(name: "david"),
CustomModel(name: "marry"),
CustomModel(name: "henry"),
CustomModel(name: "nadi"), ]
You can create an alert type and handle it using switch statement.
enum AlertType {
case selectDelete
case selectAllDelete
}
private var alertType: AlertType?
#State private var isAlertPresented = false
...
Button(action: {
alertType = .selectDelete
isAlertPresented = true
}) {
Text("remove all")
}
...
.alert(isPresented: $isAlertPresented) {
presentAlert()
}
...
func presentAlert() -> Alert {
switch alertType {
case .selectDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel())
case .selectAllDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel())
default:
return Alert(title: Text(""))
}
}
If you apply the modifier to each Button it'll work. Also, you might find confirmationDialog more suitable for this task.
Move your Buttons into custom Views will help too because body has a 10 View limit.
I have list items in SwiftUI, and when I delete list items I want to delete after alert menu, like
"do want to delete your list items, ""yes" or "no"
is it possible?
struct MyView: View {
#State private var selectedUsers: MyModel?
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(datas){ data in
MyRowView(data: data)
.contextMenu {
Button(action: {
self.delete(item: data)
}) {
Text("delete")
}
}
.onTapGesture {
selectedUsers = data
}
} .onDelete { (indexSet) in
self.datas.remove(atOffsets: indexSet)
}})
})}
private func delete(item data: MyModel) {
if let index = datas.firstIndex(where: { $0.id == data.id }) {
datas.remove(at: index)
}
}}
In the delete action you set a #State bool to true, this triggers e.g. a ConfirmationDialog – and only after confirming there, you really delete:
} .onDelete { (indexSet) in
confirmDelete = true // a #State var Bool
}})
.confirmationDialog("Do you really want to delete?", isPresented: $confirmDelete) {
Button("Delete", role: .destructive) {
selectedUsers.remove(atOffsets: indexSet)
}
Button("Cancel", role: .cancel) { }
}
I have a DataManager in which I have the following delete func:
func deleteValue(index: Int) {
storage.remove(at: index)
save()
}
And then in another view I have all my values into a Form. I wouldn't like to use the .onDelete because I would like to have an Alert that lets the user decide if he/she really wants to delete the value, but by doing this I must insert the index. How do I do this? Here's the code:
#State var showAlertDelete = false
var dm : DataManager
var deleteButton : some View {
Button(action: {
showAlertDelete = true
}) { Text("Delete").foregroundColor(Color.red)
}.alert(isPresented: $showAlertDelete, content: {
deleteValueAlert
})
}
func deleteValue(at offset: IndexSet) {
guard let newIndex = Array(offset).first else { return }
dm.deleteValue(index: newIndex)
}
var deleteValueAlert : Alert {
Alert(title: Text("Are you sure you want to delete this?"), primaryButton: Alert.Button.default(Text("Yes")){ deleteValue //Here it says that I must add the init with the index }, secondaryButton: Alert.Button.cancel(Text("No")))
}
How can I solve this? Thanks to everyone!
You can use .onDelete by also presenting an Alert when triggered, simply add:
#State private var showAlert = false
#State private var indexSetToDelete: IndexSet?
And then in your .onDelete method:
.onDelete { (indexSet) in
self.showAlert = true
self.indexSetToDelete = indexSet
}
Last step would be to add the Alert in your view body where you can call your delete method:
.alert(isPresented: $showAlert) {
Alert(title: Text("Confirm Deletion"),
message: Text("Are you sure you want to delete xxx?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteValue(indexSet: self.indexSetToDelete!) //call delete method
},
secondaryButton: .cancel())
}
I'm trying to make a simple Master-Detail-FileViewer app. In the last FileViewer view I want to have a button, which has an option to make the file favourite (every file has an "id" string, which is appended to an Environment object). When you favour them, this object is shown at the master view for quick access for the user, linking to the FileViewer view. However, when the user taps and goes there, the button is inactive - you cannot tap it and it gets black from blue. If you want to remove them from favourites, you can't.
I'd really appreciate to tell me what is wrong and how to make the button active. No error is shown and the app doesn't crash. It just doesn't work.
Thanks in advance!
The files are either "judgement" and "secondary", both have id and title properties. The second picture is the problematic one.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
NavigationView {
List {
NavigationLink(destination: JudgementsView()) {
Text("Judgements")
}
NavigationLink(destination: SecondaryView()) {
Text("Secondary acts")
}
ScrollView(.horizontal, showsIndicators: false) {
VStack {
if favouriteList.items.isEmpty {
Text("Nothing favoured")
} else {
ForEach(favouriteList.items, id: \.self) { id in
VStack {
HStack {
ForEach(judgementsTAXraw.filter {
$0.id == id
}) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title).padding()
}
}
}
HStack {
ForEach(secondaryTAXraw.filter {
$0.id == id
}) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title).padding()
}
}
}
}
}
}
}
}
}
.navigationBarTitle(Text("Test"))
}
}
}
struct JudgementsView: View {
var body: some View {
List(judgementsTAXraw, id: \.id) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title)
}
}
}
}
struct SecondaryView: View {
var body: some View {
List(secondaryTAXraw, id: \.id) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title)
}
}
}
}
struct FileViewer: View {
var file: String
#State private var showCopySheet = false
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
Text(file)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showCopySheet = true
}) {
Image(systemName: "doc.on.doc").frame(minWidth: 40)
}.actionSheet(isPresented: $showCopySheet) {
ActionSheet(title: Text("What do you want to do?"), buttons: [
.destructive(Text("favText"), action: {
if let index = self.favouriteList.items.firstIndex(of: self.file) {
self.favouriteList.items.remove(at: index)
} else {
self.favouriteList.items.append(self.file)
}
}),
.cancel()
])
}
)
}
}
Aaaand in a separate file is the object:
import Foundation
class FavouritesList: ObservableObject {
#Published var items = [String]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "FavouredItems")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "FavouredItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([String].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}
I understand there is PresentationButton and NavigationButton in order to change views in the latest SwiftUI. However I want to do a simple operation like below. When user clicks on SignIn button if credentials are correct it will sign them in but also do a segue (in this case change the view). However I could not check if they are correct in PresentationButton and I could not change the view in a normal button.
Is there another way to do that?
#IBAction func signInClicked(_ sender: Any) {
if emailText.text != "" && passwordText.text != "" {
Auth.auth().signIn(withEmail: emailText.text!, password: passwordText.text!) { (userdata, error) in
if error != nil {
//error
} else {
performSegue(withIdentifier: "toFeedActivity", sender: nil)
}
}
} else {
//error
}
}
Here's one way.
struct AppContentView: View {
#State var signInSuccess = false
var body: some View {
return Group {
if signInSuccess {
AppHome()
}
else {
LoginFormView(signInSuccess: $signInSuccess)
}
}
}
}
struct LoginFormView : View {
#State private var userName: String = ""
#State private var password: String = ""
#State private var showError = false
#Binding var signInSuccess: Bool
var body: some View {
VStack {
HStack {
Text("User name")
TextField("type here", text: $userName)
}.padding()
HStack {
Text(" Password")
TextField("type here", text: $password)
.textContentType(.password)
}.padding()
Button(action: {
// Your auth logic
if(self.userName == self.password) {
self.signInSuccess = true
}
else {
self.showError = true
}
}) {
Text("Sign in")
}
if showError {
Text("Incorrect username/password").foregroundColor(Color.red)
}
}
}
}
struct AppHome: View {
var body: some View {
VStack {
Text("Hello freaky world!")
Text("You are signed in.")
}
}
}
I had the same need in one of my app and I've found a solution...
Basically you need to insert your main view in a NavigationView, then add an invisible NavigationLink in you view, create a #state var that controls when you want to push the view and change it's value on your login callback...
That's the code:
struct ContentView: View {
#State var showView = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("*** Login in progress... ***")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showView = true
}
}) {
Text("Push me and go on")
}
//MARK: - NAVIGATION LINKS
NavigationLink(destination: PushedView(), isActive: $showView) {
EmptyView()
}
}
}
}
}
struct PushedView: View {
var body: some View {
Text("This is your pushed view...")
.font(.largeTitle)
.fontWeight(.heavy)
}
}
Try with state & .sheet
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
You can use navigation link with tags so,
Here is the code:
first of all, declare tag var
#State var tag : Int? = nil
then create your button view:
Button("Log In", action: {
Auth.auth().signIn(withEmail: self.email, password: self.password, completion: { (user, error) in
if error == nil {
self.tag = 1
print("success")
}else{
print(error!.localizedDescription)
}
})
So when log in success tag will become 1 and when tag will become 1 your navigation link will get executed
Navigation Link code:
NavigationLink(destination: HomeView(), tag: 1, selection: $tag) {
EmptyView()
}.disabled(true)
if you are using Form use .disabled because here the empty view will be visible on form and you don't want your user to click on it and go to the homeView.