SwiftUI ForEach.onDelete properly animates but then resets - swift

Trying to figure out why my ForEach loop isn’t updating. I have the model marked as ObservedObject and have done everything I could to make sure the updates were happening. I even saw that the model was being updated while printing.
class Model {
var array: [Int]
}
…
struct ModelView: View {
#ObservedObject var model: Model
var body: some View {
List {
ForEach(model.array.indices, id:\.self) { index in
…
}.onDelete(perform: delete)
}
}
The row is animating and acting like it is deleting and does delete in the model, however the deleted row animates back in and the original data set is shown!

I figured it out. I needed to make the array in my model object with the #Published property wrapper. Doing that fixed everything!

Related

How can I prevent SwiftUI from reinitializing my wrapped property just like it does with #StateObject?

From what I've read whenever you instantiate an object yourself in your view, you should use #StateObject instead of #ObservedObject. Because apparently if you use #ObservedObject, SwiftUI might decide in any moment to throw it away and recreate it again later, which could be expensive for some objects. But if you use #StateObject instead then apparently SwiftUI knows not to throw it away.
Am I understanding that correctly?
My question is, how does #StateObject communicate that to SwiftUI? The reason I'm asking is because I've made my own propertyWrapper which connects a view's property to a firebase firestore collection and then starts listening for live snapshots. Here's an example of what that looks like:
struct Room: Model {
#DocumentID
var id: DocumentReference? = nil
var title: String
var description: String
static let collectionPath: String = "rooms"
}
struct MacOSView: View {
#Collection({ $0.order(by: "title") })
private var rooms: [Room]
var body: some View {
NavigationView {
List(rooms) { room in
NavigationLink(
destination: Lazy(ChatRoom(room))
) {
Text(room.title)
}
}
}
}
}
The closure inside #Collection is optional, but can be used to build a more precise query for the collection.
Now this works very nicely. The code is expressive and I get nice live-updating data. You can see that when the user would click on a room title, the navigation view would navigate to that chat room. The chatroom is a view which shows all the messages in that room. Here's a simplified view of that code:
struct ChatRoom: View {
#Collection(wait: true)
private var messages: [Message]
// I'm using (wait: true) to say "don't open a connection just yet,
// because I need to give you more information that I can't give you yet"
// And that's because I need to give #Collection a query
// based on the room id, which I can only do in the initializer.
init(_ room: Room) {
_messages = Collection { query in
query
.whereField("room", isEqualTo: room.id!)
.order(by: "created")
}
}
var body: some View {
List(messages) { message in
MessageBubble(message: message)
}
}
}
But what I've noticed, is that SwiftUI initializes a new messages-collection every single time the user interacts with the UI in any way. Like even when an input box is clicked to give it focus. This is a huge memory leak. I don't understand why this happens, but is there a way to tell SwiftUI to only initialize #Collection once, just like it does with #StateObject?
Use #StateObject when you want to define a new source-of-truth reference type owned by that View and tied to its life-cycle. The object will be created just before the body is run for the first time and is stored in a special place by SwiftUI. If the View struct is recreated (e.g. by a parent View's body recompute by a state change) then the previous object will be set on the property instead of a new object. If the View is no longer init during body updates then the object will be deinit.
When you pass a #StateObject into a child View then in that child View uses #ObservedObject to enable the body of the child view to be recomputed when the object changes, just the same way the body of the parent View will also be recomputed because it used an #StateObject. If you use #ObservedObject for an object that was not an #StateObject in a parent View then since no View is owning it then it will be lost every time the View is init during body updates. Also, any objects that you create in the View init, like your Collection, those are immediately lost too. These excessive heap allocations could cause a leak and slows down SwiftUI. Objects the View owns must be wrapped in #StateObject to avoid this.
Lastly, don't use #StateObject for view state and certainly don't try and implement a legacy "View Model" pattern with them. We have #State and #Binding for that. #StateObject is only for model objects (as in your domain types: Book, Person etc.), loaders or fetchers.
WWDC 2020 Data Essentials in SwiftUI explains all this very nicely.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

SwiftUI - View Not Registering Input Data On Change

I created a View that displays a line graph based on an array of Doubles. The graph struct then divides the view horizontally in order for each of the points to have the same amount of separation. I am using GeometryReader and Path to make the graph. When I load the View with a given array of Doubles it displays correctly.
However, when the data changes, such as adding or removing data, the graph is not updated properly.
As an example using text, this is what the graph looks like normally (Assume it is a line graph): |-_-_-_-_|
When I add a value to the array of Doubles, the graph does appear to show that a new value was added, but the value is not recognized or read. This is what it looks like when a value is added (Again, assume it is a line graph): |-_-_-_-_ |. As you can see, it recognized there is a new value because it allocated space for the new value, but no line was shown, as it most likely does not recognize the added value. When I reload the view, such as by going back and into the view again, the graph is displayed correctly.
The big problem comes when a value is deleted. When a value is deleted, the app crashes with the error Fatal error: Index out of range. When I load the app again the graph is fixed.
I thought there might be something wrong with my deleting, so I created a test file in which I referenced an array of Doubles and buttons to add and remove values from the array, and the same behavior happened. This showed that the values put into the View were not updated properly.
This is the file I created to test the behavior of my graph:
struct GraphTest: View {
/*Data*/
#State var data: [Double] = [180, 242, 164, 202, 176, 100]
var body: some View {
VStack {
/*Struct that shows the line graph*/
LineGraph(data: data)
.frame(height: 200)
/*Button to remove the last item of array*/
Button(action: {
data.removeLast()
}) {
Text("Delete")
}
/*Button to add a value to the end of the array*/
Button(action: {
data.append(180)
}) {
Text("Add")
}
}
}
}
And this is how I'm setting up my LineGraph struct:
struct LineGraph: View {
var data: [Double]
var body: some View {
GeometryReader { geometry in
...
I tried doing data #Bindable in LineGraph, but I could not find a way to do so, as I'm getting the data from a Core Data FetchRequest, but I really do not know what to do from now to fix the issue, as I am relatively new in SwiftUI, and could not find anything similar on the internet.
Any help is appreciated, thank you :)
There is not enough code provided in LineGraph, but I assume you have inside something like
ForEach(0..<data.count) { i in // iterating this way result in crash !!
Text("\(self.data[i])")
}
instead you have to iterate at least like
ForEach(data.indices, id: \.self) { i in // no crash !!
Text("\(self.data[i])")
}

SwiftUI MacOS using ObservableObject to update State

I am having a State in SwiftUI Mac OS, which stores my active selection of a Navigation View. Everything is working with the Navigation view.
Now I have created a new class which confirms to the Observable Object. In some child views I will make a change to that object. When the change is done, my Navigation view updated the object aswell, which is the feature of Observable Object as far as I understand.
What I now want to achieve is that the Observable object changes my #State in my Navigation view.
That is my declaration in my Navigation view. UserData stores a int aswell, which should be set to the selection on change.
#EnvironmentObject var userData: UserData
#State var selection: Int?
So userData.active = 2, should set selection = 2 aswell. Is there a onChange event I can trigger?
I am using that #State selection for a Binding in my Navigation Link.
NavigationLink(destination: SecondContentView(), tag: 0, selection: self.$selection)
{
Second approach, would be using that userData.active : Int directly as State. However, I am passing that selection State as Binding and it gives me an error when passing the variable of an EnvironmentObject as Binding.
Try to use UserData directly, like
NavigationLink(destination: SecondContentView(), tag: 0,
selection: self.$userData.active)

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