Edit data within a list in SwiftUI

I think I messed something up because except the default values, I cannot see anything in my list. I want to have a TextField and a "Counter" in my list per entry which should be editable the whole time. But also the preview just yields the default values and not the ones I set preemptively in my preview construct. Changes in runtime are just ignored.

This is what I got so far, did I maybe messed up @State, @Binding and @ObservedObject again? Or did I missed something else? If I search for edit in swiftui list all I get is the EditButton solution which is not what I want.


Answered by OOPer in 621638022
I tested your code and it has multiple different issues.

@State vars needs to be managed by SwiftUI, but SwiftUI cannot manage @State in other things than View


Remove @State from your CounterLogEntry.
You may want to get Binding to the properties, we solve it later.

You should not create an instance inside init of View, neither should not do any processing with side-effects there


SwiftUI may call init at any time needed. Unfortunately, this may not make any harm in simple cases, but while developing your app, it would abruptly affect many things and shows unexpected and unpredictable behavior.

Generally, when to create an instance of @ObservedObject is a very difficult thing to solve. But in your case, you have no need to instantiate it with following the fixes below.

Remove init from your CountingView.

SwiftUI cannot detect changes in nested reference objects


Even if your Player is well-configured as ObservableObject, SwiftUI cannot observe the changes of its properties when embedded in another object CountingViewModel.
My recommendation, give up using CountingViewModel and use Player directly instead.

You may need some extension for Player which provides some functionalities of CountingViewModel.
Code Block
extension Player {
func addNewEntry() {
counterLog.append(CounterLogEntry())
}
func removeEntry(entry: CounterLogEntry) {
counterLog.removeAll(where: { e in e.id == entry.id })
}
}


This applies also to the relationship between Player and CounterLogEntry.
While CounterLogEntry is a reference type, any changes for its properties, title and count, will not cause updating UI.
My recommendation here, make CounterLogEntry a struct.

Your CounterLogEntry would look like as follows:
Code Block
struct CounterLogEntry: Identifiable { //<- Use `struct`
let id = UUID()
var title = "" //<- Remove `@State`
var count = 0 //<- Remove `@State`
}

(As always, you may need some fixes according to this change. I will show you some of them later.)

You cannot put two different buttons of default style in a row.


In my opinion, this is a bug of SwiftUI, but it still exists in Xcode 12, so we need to work with this behavior.
Add buttonStyle (other than DefaultButtonStyle) to your buttons.


So, now,

How to get the Bindings to the properties?


When you use @ObservedObject, it provides a Binding generator as a projectedValue.
Your CountingView would be as follows:



self.$player.counterLog[index].title is a Binding to the title and you can pass it to TextField.

There may be some other ways to fix your issues, but I cannot find anything significantly easier. Please try.
Accepted Answer
I tested your code and it has multiple different issues.

@State vars needs to be managed by SwiftUI, but SwiftUI cannot manage @State in other things than View


Remove @State from your CounterLogEntry.
You may want to get Binding to the properties, we solve it later.

You should not create an instance inside init of View, neither should not do any processing with side-effects there


SwiftUI may call init at any time needed. Unfortunately, this may not make any harm in simple cases, but while developing your app, it would abruptly affect many things and shows unexpected and unpredictable behavior.

Generally, when to create an instance of @ObservedObject is a very difficult thing to solve. But in your case, you have no need to instantiate it with following the fixes below.

Remove init from your CountingView.

SwiftUI cannot detect changes in nested reference objects


Even if your Player is well-configured as ObservableObject, SwiftUI cannot observe the changes of its properties when embedded in another object CountingViewModel.
My recommendation, give up using CountingViewModel and use Player directly instead.

You may need some extension for Player which provides some functionalities of CountingViewModel.
Code Block
extension Player {
func addNewEntry() {
counterLog.append(CounterLogEntry())
}
func removeEntry(entry: CounterLogEntry) {
counterLog.removeAll(where: { e in e.id == entry.id })
}
}


This applies also to the relationship between Player and CounterLogEntry.
While CounterLogEntry is a reference type, any changes for its properties, title and count, will not cause updating UI.
My recommendation here, make CounterLogEntry a struct.

Your CounterLogEntry would look like as follows:
Code Block
struct CounterLogEntry: Identifiable { //<- Use `struct`
let id = UUID()
var title = "" //<- Remove `@State`
var count = 0 //<- Remove `@State`
}

(As always, you may need some fixes according to this change. I will show you some of them later.)

You cannot put two different buttons of default style in a row.


In my opinion, this is a bug of SwiftUI, but it still exists in Xcode 12, so we need to work with this behavior.
Add buttonStyle (other than DefaultButtonStyle) to your buttons.


So, now,

How to get the Bindings to the properties?


When you use @ObservedObject, it provides a Binding generator as a projectedValue.
Your CountingView would be as follows:



self.$player.counterLog[index].title is a Binding to the title and you can pass it to TextField.

There may be some other ways to fix your issues, but I cannot find anything significantly easier. Please try.
@OOPer first of all: Thanks for your detailed explanation and help. Many of the topics you pointed out are also issues in other parts of my app (and I was already wondering why it sometimes acted in a strange manner). Learning Swift is really different compared to C Sharp (why does the hash symbol starts formatting here?) and C++. Seems like it works now as expected but I got a new question based on your response. Is there a specific reason you put the Add/Remove methods from the ViewModel into an Extension to Player instead of the Player itself? Is this just to keep it seperated or does it provide any other advantage I do not notice right now?

 Is there a specific reason you put the Add/Remove methods from the ViewModel into an Extension to Player instead of the Player itself?

Maybe just to keep it seperated would be very near what I thought.

Extension in Swift is useful to
  • Add new functionalities to an existing type

  • Group methods and properties for readability

Similar to the cases you use partial classes or extension methods in C#.

In this case, your Player looks well-made and I find you have no need to modify it.
So, I used extension to add methods taken from the ViewModel. (Easier to show what is added than putting something like //<-.)

Edit data within a list in SwiftUI
 
 
Q