UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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.

3      

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

3      

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 = "" }

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

4      

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.

3      

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.

4      

Hi @roosterboy ,

Thanks for the advice, that's useful.

I'll be posting code in future.

3      

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

3      

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

5      

@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!

3      

@Ryan  

Thanks for this nice example, it has clarified some things about ObservableObject for me. How it would work if the change to ClassB came from somewhere else other than ClassA? Here is simple example that adds a few lines to only ContentView and ClassB (ClassA and View2 remain the same as in the above example):

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)
                }
                Button("Set new ClassB value") {
                    self.classAObject.toggleLatestB()
                }
                Button("print debug") {
                    print("debug: \(String(describing: self.classAObject.items))")
                }

                List {
                    ForEach(classAObject.items) { item in
                        NavigationLink(destination: View2(classAObject: self.classAObject, classBObject: item)) {
                            HStack{
                                Text("  \(item.doubles.count) doubles")
                                Text("State: \(String(describing: item.state))")    // added
                            }
                        }
                    } // end of ForEach
                } // end of List
            }
            .navigationBarTitle(Text("View 1"))
        }
    }
}

and ClassB

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

    var shortTimer = Timer()

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

    init(title: String) {
        self.title = title
        self.startTimer()           // added
    }

    func startTimer() {             // added
        shortTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { count in
            print("timer fired")
            self.objectWillChange.send()
            self.state = false
        }
    }

}

You can see that the value is changing in memory but the list does not update. If it was updated from ClassA, it would work fine. But oftentimes it is not always the parent class making the changes.

Any ideas? ~R

3      

hi Ryan,

first, the function toggleLatestB() was not shown, but i will guess that its something like this:

func toggleLatestB() {
    if let item = items.last {
        objectWillChange.send() 
        item.state = false
    }
}

if a ClassB object gets changed somewhere outside its "owning" object of type ClassA -- your example is that the ClassB object fires a timer sometime outside of its owning ClassA object -- then the ClassA object either has to be listening for changes in the ClassB objects (using a simple Combine technique), or the ClassB object has to tell the ClassA object about the change.

in your code, the change to the state is being published, but no one is listening for change.

two options.

(1) ClassA objects retain cancellable subscriptions on all the ClassB object publishers in its items array. that requires a little bit of work, especially in cases where you want to delete ClassB objects from the items array. the general format would be something like

for each item in items {
  item.objectWillChange
    .sink(receiveValue: { _ in self.objectWillChange.send() })
    .store(in: &cancellables)
}

where cancellables is of type [AnyCancellable]Set<AnyCancellable>. this is a strongly-coupled relationship.

(2) ClassB objects issue a Notification through the default NotificationCenter that something (e.g., its state) has changed, and a ClassA object signs up to handle notifications of objects (ClassB objects) that broadcast such a change. if the ClassA object actually owns the ClassB object that issued the notification, then it just does an objectWillChange.send().

there's no strong coupling of ClassA and ClassB objects (with respect to the state variable) in using notifications, and there's a lot less management issues for keeping track of all the subscriptions (cancellables) that a ClassA object manages.

hope that helps,

DMG

3      

@Ryan  

Yes! This suggestion has been very helpful. I implemented option (1) and it works a charm. I implemented the solution only slightly differently so I will include the working code here below.

As you suggest there may be work involved in removing ClassB objects from the array, so I added a button that does that to test if it might run into trouble. Nothing too terrible seems to be happening. With the terminology from the above post I was able to find a few similar questions on StackOverflow. This post seems to suggest that maybe the memory issues take care of themselves (?): https://stackoverflow.com/questions/61889158/swift-combine-how-setanycancellable-works

This was not a trivial solution to what must be a fairly common occurrence during development; I can imagine many scenarios where it might occur. Several posts have replies that say something like, "Nested ObservableObjects is not supported yet." Whether or not this is supported in the future depends entirely on SwifUI framework, right? So am I right to assume that were it to be supported in the future, the announcement would come during WWDC and not from an incremental bump in Swift itself?

Thanks agin for the help and suggestions.

Code below--->

ContentView:

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)
                }
                Button("Set new ClassB value") {
                    self.classAObject.toggleLatestB()
                }
                Button("Remove Last") {
                    if (self.classAObject.items.count > 0){
                        self.classAObject.items.last?.shortTimer.invalidate()
                        self.classAObject.items.removeLast()
                    }
                }
                List {
                    ForEach(classAObject.items) { item in
                        NavigationLink(destination: View2(classAObject: self.classAObject, classBObject: item)) {
                            HStack{
                                Text("  \(item.doubles.count) doubles")
                                Text("State: \(String(describing: item.state))")    // added
                            }
                        }
                    } // end of ForEach
                } // end of List
            }
            .navigationBarTitle(Text("View 1"))
        }
    }
}

ClassA:

import Foundation
import Combine

class ClassA: ObservableObject {

    @Published var items = [ClassB]()
    private var cancellables = Set<AnyCancellable>()

    func appendNewB(title: String) {
        let newItem = ClassB(title: title)

        newItem.objectWillChange
            .sink(receiveValue: { _ in
                self.objectWillChange.send()
            })
            .store(in: &cancellables)
        items.append(newItem)
    }

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

    func toggleLatestB() {
        objectWillChange.send() // lets View 1 know about the change
        items.last?.state.toggle()
    }

}

ClassB:

import Foundation

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

    var shortTimer = Timer()

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

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

    func startTimer() {
        shortTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { count in
            self.state.toggle()
            print("timer fired, state is: \(String(describing: self.state))")
        }
    }

}

3      

hi Ryan,

glad to hear this works for you. although you did mention:

As you suggest there may be work involved in removing ClassB objects from the array

if you delete a ClassB object, i think the question remains as to whether the cancellable you created and attached to the object is (1) cancelled and unlinked from the ClassB object; and then (2) removed from the Set that you own.

it seems like (1) should happen: what you subscribed to is gone. i'm not convinced that (2) happens on its own. you may just have a dead cancellable hanging around in the Set of cancellables that does nothing.

anyway, you could print out the count of the Set of cancellables to see if the count is right after a deletion. that will tell you. and if you need to remove a dead cancellable, i'd offer a simple strategy: remove all the cancellables; remove the item for the items array; then re-establish a whole new Set of cancellables.

ADDED AFTER INITIAL POST: in my current experimental project, i opted for the second solution i suggested to your problem: use NotificationCenter and not worry about managing the cancellables. you can find that project on Github, called ShoppingList.

glad to have helped,

DMG

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.