NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

ObservableObject using classes and arrays.

Forums > SwiftUI

I have two classes, class A and class B.

Class A holds an array of type class B.

Class B holds a string, and array of type Double.

I have two views.

View 1 displays a button which when clicked, appends class B to the array in class A.

View 2 displays a button whcih when clicked, appends a new Double (0.0) to the array in class B. The view also has a ForEach, displaying text for each double in the array in class B.

Currently, when I append a double to the array in class B, the ForEach does not list any new items. However, if I then append a new class to the array in classA, my previously added doubles all appear.

I've tried various combinations of @ObservedObject with no luck.

The desired effect is that each time I append to classB with a new double, the ForEach list would update. If I then add a new class to the array in classA, I have a second view with another ForEach listing the doubles in the new array.

I'm in need of some advice and would love to know where I'm going wrong.

   

hi,

some code from you would be very helpful, to see where a ClassA item is instantiated (is it a var in View1 ?) and where a ClassB item lives (is one of them a var in View2 ?). and do you have a navigation link that gets you from View1 to View2?

looking forward to seeing more ...

DMG

   

Hi @delawaremathguy thanks for your help.

I've tried to upload some screenshots, but for some reason the site is not allowing it. I've posted some code below, which is hopefully slightly helpful.

Class A is instantiated outside of the view structure in another Swift file.

Class B is instantiated in view 1, and is passed into view 2 when the view is called.

for example in view 1 'ContentView': var newClass = ClassB() 'TestView(myClass: newClass)'

for example in view 2 'TestView: @State var myClass: ClassB

ForEach(myClass.array2, id: .self) { item in Text("(item)") }

Then, for example in the other swift file.

**class A { var array1 = [ClassB]() }

class B { var array2 = [Double]() var myName: String = "" }

   

Subscribe to Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

hi,

see if the code below is sort of what you had in mind:

class ClassA: ObservableObject {
    @Published var items = [ClassB]()
    func appendNewB(title: String) {
        objectWillChange.send()
        items.append(ClassB(title: title))
    }
}

class ClassB: ObservableObject, Identifiable {
    var id = UUID()
    @Published var title = ""
    @Published var doubles = [Double]()
    func appendNewDouble() {
        objectWillChange.send()
        doubles.append(0.0)
    }

    init(title: String) {
        self.title = title
    }
}

struct ContentView: View {
    @ObservedObject var classAObject = ClassA()
    var body: some View {
        NavigationView {
            VStack {
                Button("Append a new ClassB to classA object") {
                    self.classAObject.appendNewB(title: Date().description)
                }
                List {
                    ForEach(classAObject.items) { item in
                        NavigationLink(destination: View2(classBObject: item)) {
                            Text(item.title)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("View 1"))
        }
    }

}

struct View2: View {
    @ObservedObject var classBObject: ClassB
    var body: some View {
        VStack {
            Button("Append a new double") {
                self.classBObject.appendNewDouble()
            }
            List {
                ForEach(classBObject.doubles, id:\.self) { double in
                    Text(String(double))
                }
            }
        }
        .navigationBarTitle(Text(classBObject.title), displayMode: .inline)
    }
}

looks like it works. i set titles by using a date string -- and i'm not sure that you need to explicitly do objectWillChange.send() messages when appending a new item.

hope that helps!

DMG

1      

Hi @delawaremathguy

Huge thanks for posting the code.

I'll have a play around.

That's exactly what i was trying to achieve.

Thank you.

   

I've tried to upload some screenshots

FYI, you shouldn't post screenshots of your code, you should post the actual code itself. Screenshots can be hard to read and one can't copy the code from them into a playground or Xcode for testing possible solutions, meaning anyone trying to help you would need to retype everything.

To post code, simply put three backticks ``` on the line before and the line after your code and it'll be nicely formatted like in @delawaremathguy's post.

1      

Hi @roosterboy ,

Thanks for the advice, that's useful.

I'll be posting code in future.

   

Hello everyone,

this is super interesting thread that really helped me to figure some things in my currrent coding challenge. Thank you!

@delawaremathguy, I wonder how would you approach a scenario where a View2 has to observe/modify only certain parts of classBObject (eg. title). In my situations View2 is using @State for title and is passing that even further to UIViewRepresentable through overlay.

Can I do this passing further? Is it possible to use a part of classBObject and change it to a @State? Should I use different solution like EnvironmentObject? Any thougths?

class ClassA: ObservableObject {
    @Published var items = [ClassB]()
    func appendNewB(title: String) {
        objectWillChange.send()
        items.append(ClassB(title: title))
    }
}

class ClassB: ObservableObject, Identifiable {
    var id = UUID()
    @Published var title = ""
    @Published var doubles = [Double]()
    func appendNewDouble() {
        objectWillChange.send()
        doubles.append(0.0)
    }

    init(title: String) {
        self.title = title
    }
}

struct ContentView: View {
    @ObservedObject var classAObject = ClassA()

    var body: some View {
        NavigationView {
            VStack {
                Button("Append a new ClassB to classA object") {
                    self.classAObject.appendNewB(title: Date().description)
                }
                List {
                    ForEach(classAObject.items) { item in
                        View2(title: item.title)) 
                    }
                }
            }
            .navigationBarTitle(Text("View 1"))
        }
    }

}

struct View2: View {
    @State var title: String
    var body: some View {
        VStack {
          Text(title)
            }.overlay(gestureLong(title: $title)
    }
}

   

hi,

first, i'd like to make a correction to the code i posted previously, because it has a somewhat obvious omission: View1 only shows titles of classB objects; View2 only shows the doubles associated with ClassB objects; but View1 is never aware of changes to ClassB objects. you don't see this right away, because View1 does not display any information about the doubles associated with each of the ClassB objects (it only shows titles, none of which is changed in View2).

(i'll get to your @State question below)

so, let's fix that, by making sure that we treat ClassB objects as owned by a ClassA object. so, we don't make changes to ClassB objects without telling the ClassA object to make those changes for us. ClassA needs a method to do that.

class ClassA: ObservableObject {
    @Published var items = [ClassB]()

    func appendNewB(title: String) {
        items.append(ClassB(title: title))
    }

    func appendDouble(to item: ClassB) {
        objectWillChange.send() // lets View 1 know about the change
        item.appendNewDouble()
    }
}

there is no need for an objectWillChange.send() call in appendNewB, since the items array will change and publish that change without explicitly doing it ourself. the ClassB code will similarly remove the explicit objectWillChange.send() call.

class ClassB: ObservableObject, Identifiable {
    var id = UUID()
    @Published var title = ""
    @Published var doubles = [Double]()

    func appendNewDouble() {
        doubles.append(0.0)
    }

    init(title: String) {
        self.title = title
    }
}

i have rewritten the View1 display to show both a title AND the number of doubles associated with a ClassB object -- the previous code was not showing the number of doubles because View1 never learned about any changes. i'll also now pass along to View2 both the ClassA object that owns the data, along with what ClassB object is to be displayed.

struct ContentView: View {
    @ObservedObject var classAObject = ClassA()
    var body: some View {
        NavigationView {
            VStack {
                Button("Append a new ClassB to classA object") {
                    self.classAObject.appendNewB(title: Date().description)
                }
                List {
                    ForEach(classAObject.items) { item in
                        NavigationLink(destination: View2(classAObject: self.classAObject, classBObject: item)) {
                            HStack{
                                Text(item.title)
                                Text("  \(item.doubles.count) doubles")
                            }
                        }
                    } // end of ForEach
                } // end of List
            }
            .navigationBarTitle(Text("View 1"))
        }
    }
}

finally, the View2 code must now accept the ClassA object (so it knows there is a change to something it owns) AND make the ClassB object an @ObservedObject so the view will redraw when the doubles array is changed.

struct View2: View {
    var classAObject: ClassA
    @ObservedObject var classBObject: ClassB
    var body: some View {
        VStack {
            Button("Append a new double") {
                self.classAObject.appendDouble(to: self.classBObject)
            }
            List {
                ForEach(classBObject.doubles, id:\.self) { double in
                    Text(String(double))
                }
            }
        }
        .navigationBarTitle(Text(classBObject.title), displayMode: .inline)
    }
}

whew! code cleanup is always a good thing.

now about that @State question.

you are passing in an item's title and using that as a @State variable, but a String is a value type in Swift; so your View2 will have a copy of the title. View2 will work fine on its own, but any changes you make to the title will be local to the view, and will not propagate back to the ClassB item it came from.

so, perhaps a similar approach could work: methods available in the ClassA object to make changes to a ClassB object.

alternatively, if you think it seems a little unwieldy to keep going through the ClassA object to make a change to one of its ClassB items, you might just call ClassB methods directly and, with each modification, "tell the ClassA object that owns me that i have changed." this could be done either by keeping a reference in each B back to the owning ClassA (ouch ... memory cycles ... the reference back to ClassA should be weak or unowned); or, ClassA could subscribe to each of its ClassB objects to watch for changes using Combine.

that's about all i have for you right now.

BTW: this coordination stuff in SwiftUI started out being a little mysterious, then got better, then got worse, and even now has me occasionally rethinking everything. if you'd like to follow my current thinking on this stuff, see my ShoppingList project project. it's my "fail out-in-public" project that plays with these notions.

hope that help,

DMG

1      

@delawaremathguy,

you have a great talent of explaining complex things, Thank you so much for this thorough explanation!

It gave me a better understanding of my options. Before you wrote this I tried to work with EnvironmentObject and it helped to simplify a lot of data passing in my case. I hope it will be enough and sustainable in the long term.

I will also try to use the template you wrote here for next project. It is really useful!

One more time, THANK YOU!

   

Subscribe to Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

Not logged in

Log in
 

Link copied to your pasteboard.