How can I delete a list item in SwiftUI from a child view without using onDelete(perform:)? - swift

I have a chat feature in my app that allows you to report and block someone from communicating with you any further.
Here's my InboxView.swift that shows a user's conversations:
List(Array(conversations.conversations.enumerated()), id: \.1.id){ (index, conversation) in
VStack{
NavigationLink(destination: ChatView(conversation_id: conversation.id, avatar: conversation.avatar, displayName: conversation.displayName, user_id: conversation.receiver_id, parentIndex: index)){
ConversationList(id : conversation.id, user_id : conversation.user_id, receiver_id : conversation.receiver_id, lastMessage : conversation.lastMessage, avatar : conversation.avatar, displayName : conversation.displayName, startedAt : conversation.startedAt)
}
Divider()
}
}
The above code simply provides the end-user an interface for them to select which conversation they want to go into. Here's where things get tricky with the following view diagram:
InboxView --> ChatView --> ProfileView
Each --> represents a NavigationLink that leads to the subsequent view. On the ProfileView.Swift page, I present a button in which the end-user can block the person they are talking to. I have already figured out how to take the user back to InboxView with a series of
#Environment(\.presentationMode) var mode
and
self.mode.wrappedValue.dismiss()
but for convenience, I also want to delete the list item that was associated with the blocked user's conversation.
How can I tell InboxView which ChatView triggered the delete request and pass that through a function like this?
func removeRow(at offsets: IndexSet){
if let first = offsets.first {
let conversationRemoving = conversations.conversations[first]
conversations.conversations.remove(at:first)
}
}
I don't see in the documentation for presentationMode to trigger a function via wrappedValue

It could be done directly inside List (as we have access to index in it) and remove record from already fetched results.
If person model would have specific field (say blocked), then it could be like below (in pseudo-code, to be shorter):
List(Array(conversations.conversations.enumerated()), id: \.1.id){ (index, conversation) in
VStack{
NavigationLink(destination: ChatView(...)) {
ConversationList(...)
}
Divider()
}
.onAppear { // called on show and on return back
if conversation.receiver.blocked { // << here !!
// better to do it asynchronously
DispatchQueue.main.async {
self.conversations.conversations.remove(at: index) // << here !!
}
}
}
}

Related

SwiftUI -- Handling Interactions in Form

I'm creating a more complex UI that consists of several sub-parts, each sub-part is enclosed in a Section and all sections have to go in one Form
As soon as several Buttons belong to one Section, each action of the Buttons is triggered as soon as one Button is clicked.
In the following code
struct ContentView: View {
#State private var cnt = 0
var body: some View {
Form {
Section (header: Text("Counter")){
HStack {
Button("Increment"){
print("Inc")
cnt += 1
}
Spacer()
Button("Decrement"){
print("Dec")
cnt -= 1}
}
Text("Counter : \(cnt)")
}
}
}
}
a tab one one of the two Buttons triggers both actions. By adding .buttonStyle(PlainButtonStyle()) to each Button or by removing the Form everything works as expected. Unfortunately the real UI is more complex and relies on the Form widget. The .buttonStyle(PlainButtonStyle()) removes the visual hint that the elements are active.
Is there a different way to solve my problem?

Pulling from database on button click SWIFT UI

I have a create post function that adds a document to my firebase database and I am trying to fetch it and display it to the user on the front end when the post has been successfully added. The way I'm trying to do this is fetching changes on click of a button, but because the function to create a post is in the same button they execute at the same time (before the post has been successfully created) and ends up not showing the newly created post.
How would I do this - would it be a completion handler or state variable change or?
CODE BELOW:
Button {
viewModel.uploadPost(withCaption: caption)
presentationMode.wrappedValue.dismiss()
showAlert.toggle()
viewModel.fetchposts()
} label: {
Text("Post")
.bold()
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBlue))
.foregroundColor(.white)
.clipShape(Capsule())
}
The reason why the upload posts and fetch posts are getting called at the same is because you are using asynchronous way of calling the methods. Instead, wrap the callbacks inside a Task struct. It will allow you wrap and use await to run asynchronously. Or you could also wrap them in different Task instances and call them using await.
struct ContentView: View {
var body: some View {
Button {
Task {
await uploadPost()
await fetchPosts()
}
}
}
}
Documentation for Task

SwiftUI array.isEmpty do not show until received array from Firestore

Currently have a VStack populating with items from firestore and am trying to display a message if there are no items in the db. It works perfectly but there is a brief second where it shows the "No Items" message before loading the list of items. I understand it simply hasn't gotten the results yet from the db. Is there any way to hold off showing ANYTHING until that call is complete?
VStack(alignment: .leading){
if itemList.isEmpty {
Text(“No items”)
} else {
ForEach(itemList.indices, id: \.self) { i in
Text(itemList[I].item)
}
}
}
.onAppear() { self.readItems() }
No, you can hold rendering until there is data; that might take way too long. Your code has to always render something, no matter what state the app is in.
But what you can do is set a variable to signal whether data has been loaded, like isLoaded, set it initially to false, set it to true in the completion handler, and use that to determine what to show. Most commonly, you'll show a single Text view Loading... in that case, and then switch over to what you now have once isLoaded is true.

Selecting a picker value to lead to a text field, SwiftUI

Im trying to implement a feature in my app.
When I click on my picker:
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
This takes me to a screen and then I select the value (this is fine - it works as expected)
How could I select the value which then takes my to let's say another screen so I can fill in more details regarding the selected value.
For example the above picker has a values to select eduction level, after selecting it, how could I get an action sheet/another screen appear so I can have a text field there to save this extra data to or once the selection is made, a text field appears for me to save some extra data, and then clicking a button which would take me to the original screen of the picker (hope that makes sense)?
I've tried researching online for a problem similar to this but can't seem to find one/or if you can point me in the direction of what I should be looking into?.
Tried the following:
If I correctly understood your scenario here is a possible approach (replication tested with Xcode 12 / iOS 14)
Picker(selection: $profileViewModel.education,
label: Text("Education Level")) {
ForEach(Education.levels, id: \.self) { level in
Text(level).tag(level)
}
}
.onChange(of: profileViewModel.education) { _ in
DispatchQueue.main.async {
self.showSheet = true
}
}
.sheet(isPresented: $showSheet) {
// put here your text editor or anything
Text("Editor for \(profileViewModel.education)")
}

Why is my SwiftUI List row not always updating internally

I have a list of reminders grouped into sections by completion and date. With data coming from an ObservedObject DataStore called global. I pass a realmBinding to the cell. The cell can update this binding and it will trigger the data store to update.
List {
// Past Due
if self.global.pastDueReminders.count > 0 {
Section(header: SectionHeader {}){
ForEach(self.global.pastDueReminders) { reminder in
NavigationLink(destination: ReminderDetail( reminder: reminder.realmBinding())) {
GeneralReminderCell(reminder: reminder.realmBinding())
}
}
}
}
// Completed
if self.global.completeReminders.count > 0 {
// Same as PastDue but for Completed
}
}
The cell looks something like:
struct GeneralReminderCell: View {
#Binding var reminder:Reminder
var body: some View {
HStack(alignment:.top, spacing: 10) {
Image(systemName: reminder.completed ? "checkmark.circle.fill" : "circle")
.onTapGesture(perform:{ self.reminder.completed = !self.reminder.completed })
VStack(alignment: .leading, spacing: 2) {
Text("Follow up with \(reminder.client.fullName)").fontWeight(.semibold)
if reminder.title.count > 0 {
Text(reminder.title)
}
Text(reminder.date.formatted()).foregroundColor(.gray)
}
}.padding(.vertical, 10)
}
}
When tapping on an image it toggles the reminder completion state and its position changes in the List view. The image that was tapped should changed to a filled in check when completed.
This behaviour almost always happens as expected, but sometimes the checked image will get out of sync with the completed state of reminder. I've look at this for quite some time and have not made much headway. Why is the checked image not always matching the state of the data?
Even though this is a very old question, I may have been working on what appears to be this same problem. In my case, my App is running on macOS. At first, the problem also seemed to be very intermittent and had been difficult to reproduce consistently.
I also have a View with a ForEach supplying rows to a List. My row's View contains an #State for an Optional Image that gets updated several different ways via actions performed by that same row View (e.g. Continuity Camera, file picker or drag & drop). The issue is that sometimes the new Image is shown and sometimes it is not shown. Using Self._printChanges() I am able to see that the #State is changing and the row's body it is being redrawn, however, the rendered View does not change. The only pattern that I am able to observe is that this issue only seems to occur with the last row in the List. Based on the success of my workaround below, it seems to confirm that there is an issue with the way SwiftUI's List reuses table cells.
My solution/workaround is to replace:
List {
ForEach {
}
}
With:
ScrollView {
LazyVStack {
ForEach {
}
}
}