SwiftUI List Rows Not Refreshed After Updating Binding In Other Screen - ios

I have a simple watchOS SwiftUI Application. The application has three screens. The first screen consists of a List of items. When you press that item, it will redirect to another screen & when you tap a button there it will open up a .sheet View which allows you to edit the item in the list.
The first view looks like this:
class Object: NSObject {
var title: String
init(title: String) {
self.title = title
}
}
struct Row: View {
#Binding var object: Object
var body: some View {
Text(self.object.title)
}
}
struct ContentView: View {
#State private var objects = [Object]()
var body: some View {
NavigationView {
List {
ForEach(objects.indices, id: \.self) { idx in
NavigationLink(destination: SecondView(object: self.$objects[idx])) {
Row(object: self.$objects[idx])
}
}
}
}
.onAppear {
self.objects = [
Object(title: "Test 1"),
Object(title: "Test 2")
]
}
}
}
These are the second & third views:
struct SecondView: View {
#Binding var object: Object
#State private var showPicker: Bool = false
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.showPicker.toggle()
}) {
Text("Press Here")
}
}
.sheet(isPresented: $showPicker) {
ThirdView(object: self.$object)
}
}
}
struct ThirdView: View {
#Binding var object: Object
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.update()
}, label: {
Text("Tap here")
})
}
}
func update() {
let newObj = self.object
newObj.title = "Hello, World!"
self.object = newObj
}
}
I'd expect, whenever I tap the button in the third view, the Binding (and thus the State) get's updated with "Hello, World". However, that is not the case, although not immediately.
What I currently see happening is that when I tap the button in the third view, the Text in that view does not get updated. When I dismiss the third view and go back to the second view, I do see "Hello, World". But when I go back to the list, the row still has the old value.
One other thing I noticed is that, when I fill the array of objects directly, like so:
#State private var objects = [Object(title: "Test 1"), Object(title: "Test 2")]
and remove the filling of the array in .onAppear, this work totally how I'd expect it to (everything updates immediately to "Hello, World".
Does anyone one know what I'm doing wrong here or did I might hit a bug?
Thanks!

Complex objects need to be classes conforming to #ObservableObject.
Observed ivars need to be published.
class Object: ObservableObject {
#Published var title: String
[...]
}
Observing views would use them as #ObservedObject
struct Row: View {
#ObservedObject var object: Object
[...]
}
You might have to create an object wrapper for your lists
class ObjectList: ObservableObject {
#Published var objects: [Object]
[...]
}

Related

The body of view to be destroyed gets called (but it shouldn't)

While verifying how binding invalidates a view (indirectly), I find an unexpected behavior.
If the view hierarchy is
list view -> detail view
it works fine (as expected) to press a button in the detail view to delete the item.
However, if the view hierarchy is
list view -> detail view -> another detail view (containing the same item)
it crashes when I press a button in the top-most detail view to delete the item. The crash occurs in the first detail view (the underlying one), because its body gets called.
To put it in another way, the behavior is:
If the detail view is the top-most view in the navigation stack, its body doesn't get called.
Otherwise, its body gets called.
I can't think out any reason for this behavior. My debugging showed below are what happened before the crash:
I pressed a button in top-most detail view to delete the item.
The ListView's body got called (as a result of ContentView body got called). It created only the detail view for the left item.
Then the first DetailView's body get called. This is what caused the crash. I can't think out why this occurred, because it certainly didn't occur for the top-most detail view.
Below is the code. Note the ListView and DetailView contains only binding and regular properties (they don't contain observable object or environment object, which I'm aware complicate the view invalidation behavior).
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
#Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#Binding var foos: [Foo]
var fooID: Int
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(fooID)
return VStack {
Text(label)
Divider()
Text("Value: \(foos.get(fooID).value)")
NavigationLink {
DetailView(foos: $foos, fooID: fooID, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(fooID)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}
Test 1: Start the app, click on an item in the list view to go to the detail view, then click on "Delete It" button. This works fine.
The view hierarchy: list view -> detail view
Test 2: Start the app, click on an item in the list view to go to the detail view, then click on "Create another detail view" to go to another detail view. Then click on "Delete It" button. The crashes the first detail view.
The view hierarchy: list view -> detail view -> another detail view
Could it be just another bug of #Binding? Is there any robust way to work around the issue?
You need to use your data model rather than performing procedural code in your views. Also, don't pass items by id; Just pass the item.
Because you use the id of the Foo instead of the Foo itself, and you have a force unwrap in your get function, you get a crash.
If you refactor to use your model and not use ids it works as you want.
You don't really need your array extension. Specialised code as an extension to a generic object doesn't look right to me.
The delete code is so simple you can just handle it in your model, and do so safely with conditional unwrapping.
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
func delete(foo: Foo) {
if let index = firstIndex(where: { $0.id == id }) {
self.foos.remove(at: index)
}
}
}
struct ListView: View {
#ObservedObject var model: DataModel
var body: some View {
NavigationView {
List {
ForEach(model.foos) { foo in
NavigationLink {
DetailView(model: model, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#ObservedObject var model: DataModel
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(model: model, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
model.delete(foo:foo)
}
}
}
}
I think this is very much like Paul's approach. I just kept the Array extension with the force unwrap as in OP.
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2), Foo(id: 3, value: 3)]
}
struct ListView: View {
#EnvironmentObject var dataModel: DataModel
var body: some View {
NavigationView {
List {
ForEach(dataModel.foos) { foo in
NavigationLink {
DetailView(foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var dataModel: DataModel
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(foo: foo, label: "Yet Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
dataModel.foos.remove(foo.id)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView()
.environmentObject(dataModel)
}
}
Here is a working version. It's best to pass the model around so you can use array subscripting to mutate.
I also changed your id to UUID because that's what I'm used to and changed some vars that should be lets.
import SwiftUI
struct Foo: Identifiable {
//var id: Int
let id = UUID()
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
//extension Array where Element == Foo {
// func get(_ id: Int) -> Foo {
// return first(where: { $0.id == id })!
// }
//
// mutating func remove(_ id: Int) {
// let index = firstIndex(where: { $0.id == id })!
// remove(at: index)
// }
//}
class DataModel: ObservableObject {
//#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
#Published var foos: [Foo] = [Foo(value: 1), Foo(value: 2)]
func foo(id: UUID) -> Foo? {
foos.first(where: { $0.id == id })
}
}
struct ListView: View {
//#Binding var foos: [Foo]
#StateObject var dataModel = DataModel()
var body: some View {
NavigationView {
List {
//ForEach(foos) { foo in
ForEach(dataModel.foos) { foo in
NavigationLink {
//DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
DetailView(dataModel: dataModel, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
//#Binding var foos: [Foo]
#ObservedObject var dataModel: DataModel
//var fooID: Int
let foo: Foo
let label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
//print(fooID)
print(foo.id)
return VStack {
Text(label)
Divider()
//Text("Value: \(foos.get(fooID).value)")
if let foo = dataModel.foo(id:foo.id) {
Text("Value: \(foo.value) ")
}
NavigationLink {
DetailView(dataModel: dataModel, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
//foos.remove(fooID)
if let index = dataModel.foos.firstIndex(where: { $0.id == foo.id } ) {
dataModel.foos.remove(at: index)
}
}
}
}
}
struct ContentView: View {
// no need for # here because body doesn't need to update when model changes
//#StateObject var dataModel = DataModel()
var body: some View {
//ListView(foos: $dataModel.foos)
ListView()
}
}
This is a version that uses Paul's approach but still uses binding. Note both versions don't really "solve" the issue (the behavior I described in my original question still exists) but instead "avoid" the crash by not accessing data model when rendering the view hierarchy in the body. I think this is a key point to use a framework successfully - don't fight it.
Regarding the use of binding in the code example, I'm aware most people use ObservableObject or EnvironmentObject. I used to do that too. I noticed the use of binding in Apple's demo app. But I may consider to switch back to the view model approach.
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
#Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#Binding var foos: [Foo]
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(foo.id)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

Complete list is recreates all views even if just one item has changed

I have a very simple schoolbook example of a SwiftUI List view that renders items from data in an array. Data in the array is Identifiable. But, when I change the the data in the array, add or remove a item then all rows in the list view are recreated. Is that correct? My understanding was that Identifiable should make sure that only the view in the list that are changed are recreated.
My list is inside a navigation view and each row links to a detail view. The problem is that since all items in the list are removed and recreated every time the data is changed then if that that happens when Im in a detail view (it's triggered by a notification) then Im thrown out back to the list.
What am I missing?
Edit: Added code example
This is my data struct:
struct Item: Identifiable {
let id: UUID
let name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
This is my ItemView
struct ItemView: View {
var item: Item
init(item: Item) {
self.item = item
print("ItemView created \(self.item.id)")
}
var body: some View {
Text(self.item.name)
}
}
An finally my list view:
struct KeyList: View {
#State var items = [Item(name: "123"), Item(name: "456"), Item(name: "789")]
var body: some View {
VStack {
List(self.items) { item in
ItemView(item: item)
}
Button(action: {
self.items.append(Item(name: "New"))
}) {
Text("Add")
}
}
}
}
When I press add it will print "ItemView created" 4 times. My understanding is that it should only do it 1 time?
Here is an example of how this could work. Tested and working on iOS 13.5
The List doesn't get recreated again when only one item is being removed. So this was accomplished.
About the poping of the View this has already been answered here:
SwiftUI ForEach refresh makes view pop
I have here a small workaround for this problem. Add the items you want to remove to an array. Then when going back, remove these items (Which will make the view pop) or go back programmatically and nothing gets removed
struct ContentView: View {
#State var text:Array<String> = ["a", "b", "c"]
var body: some View {
NavigationView() {
VStack() {
List() {
ForEach(self.text, id: \.self){ item in
NavigationLink(destination: SecondView(textItem: item, text: self.$text)) {
Text(item)
}
}
}
Button(action: {
self.text.remove(at: 0)
}){
Text("Remove \(self.text[0])")
}
}
}
}
}
struct SecondView: View {
#State var textItem: String
#Binding var text: Array<String>
#State var tmpArray: Array<String> = []
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack() {
Text(self.textItem)
Button(action: {
//Append to a tmp array which will later be used to determine what to remove
self.tmpArray.append(self.text[0])
}){
Text("Remove \(self.text[0])")
}
Button(action: {
if self.tmpArray.count > 0 {
//remove your stuff which will automatically pop the view
self.text.remove(at: 0)
} else {
// programmatically go back as nothing has been deleted
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("Go Back")
}
}
}
}

SwiftUI Published variable doesn't trigger UI update

I have an app with a navigation view list that doesn't update when new elements get added later on in the app. the initial screen is fine and everything get triggered at this moment no matter how I code them, but beyond that, it stays that way. At some point I had my "init" method as an .onappear, and dynamic elements wouldn't come in, but the static ones would get added multiple times when I would go back and forth in the app, this is no longer part of my code now though.
here what my content view look like, I tried to move the navigation view part to the class that has the published var, in case it help, visually it dint change anything, dint help either.
struct ContentView: View {
#ObservedObject var diceViewList = DiceViewList()
var body: some View {
VStack{
Text("Diceimator").padding()
diceViewList.body
Text("Luck Selector")
}
}
}
and the DiceViewList class
import Foundation
import SwiftUI
class DiceViewList: ObservableObject {
#Published var list = [DiceView]()
init() {
list.append(DiceView(objectID: "Generic", name: "Generic dice set"))
list.append(DiceView(objectID: "Add", name: "Add a new dice set"))
// This insert is a simulation of what add() does with the same exact values. it does get added properly
let pos = 1
let id = 1
self.list.insert(DiceView(objectID: String(id), dice: Dice(name: String("Dice"), face: 1, amount: 1), name: "Dice"), at: pos)
}
var body: some View {
NavigationView {
List {
ForEach(self.list) { dView in
NavigationLink(destination: DiceView(objectID: dView.id, dice: dView.dice, name: dView.name)) {
HStack { Text(dView.name) }
}
}
}
}
}
func add(dice: Dice) {
let pos = list.count - 1
let id = list.count - 1
self.list.insert(DiceView(objectID: String(id), dice: dice, name: dice.name), at: pos)
}
}
I'm working on the latest Xcode 11 in case it matter
EDIT: Edited code according to suggestions, problem didnt change at all
struct ContentView: View {
#ObservedObject var vm: DiceViewList = DiceViewList()
var body: some View {
NavigationView {
List(vm.customlist) { dice in
NavigationLink(destination: DiceView(dice: dice)) {
Text(dice.name)
}
}
}
}
}
and the DiceViewList class
class DiceViewList: ObservableObject {
#Published var customlist: [Dice] = []
func add(dice: Dice) {
self.customlist.append(dice)
}
init() {
customlist.append(Dice(objectID: "0", name: "Generic", face: 1, amount: 1))
customlist.append(Dice(objectID: "999", name: "AddDice", face: 1, amount: 1))
}
}
SwiftUI is a paradigm shift from how you would build a UIKit app.
The idea is to separate the data that "drives" the view - which is the View model, from the View presentation concerns.
In other words, if you had a ParentView that shows a list of ChildView(foo:Foo), then the ParentView's view model should be an array of Foo objects - not ChildViews:
struct Foo { var v: String }
class ParentVM: ObservableObject {
#Published let foos = [Foo("one"), Foo("two"), Foo("three")]
}
struct ParentView: View {
#ObservedObject var vm = ParentVM()
var body: some View {
List(vm.foos, id: \.self) { foo in
ChildView(foo: foo)
}
}
}
struct ChildView: View {
var foo: Foo
var body = Text("\(foo.v)")
}
So, in your case, separate the logic of adding Dice objects from DiceViewList (I'm taking liberties with your specific logic for brevity):
class DiceListVM: ObservableObject {
#Published var dice: [Dice] = []
func add(dice: Dice) {
dice.append(dice)
}
}
struct DiceViewList: View {
#ObservedObject var vm: DiceListVM = DiceListVM()
var body: some View {
NavigationView {
List(vm.dice) { dice in
NavigationLink(destination: DiceView(for: dice)) {
Text(dice.name)
}
}
}
}
If you need more data than what's available in Dice, just create a DiceVM with all the other properties, like .name and .dice and objectId.
But the takeaway is: Don't store and vend out views. - only deal with the data.
While testing stuff I realized the problem. I Assumed declaring #ObservedObject var vm: DiceViewList = DiceViewList() in every other class and struct needing it would make them find the same object, but it doesn't! I tried to pass the observed object as an argument to my subview that contain the "add" button, and it now work as intended.

Resources