0

I can't get an @StateObject variable correctly initialized within the main view init() routine. It looks as if the object gets reinitialized at every modification and ends up being unpopulated when the ui gets ready. Here is the code:


import SwiftUI

class Node: Identifiable {
    var id = UUID()
    var x = 0.0
    var y = 0.0
    func toString() -> String {
        return "\(id):(\(x), \(y))"
    }
}

class Objects: ObservableObject {
    @Published var nodes: [Node] = []
    @Published var show: Bool = false
    
    func addNode(_ node: Node) {
        self.nodes.append(node)
        print(self.nodes.count)
    }
    
    func toString() -> String {
        var s = "Nodes: (\(self.nodes.count))\n"
        for node in self.nodes {
            s += node.toString() + "\n"
        }
        return s
    }
}

struct ContentView: View {
    @StateObject var objects = Objects()
    
    init() {
        var node = Node()
        node.x = 50
        node.y = 200
        print(node.toString())
        objects.addNode(node)
        node = Node()
        node.x = 200
        node.y = 100
        print(node.toString())
        objects.addNode(node)
        objects.show.toggle()
        print(objects.toString())
    }
    
    var body: some View {
        Button("Add") {
            let i = objects.nodes.count
            let node = Node()
            node.x = Double(i)
            node.y = Double(i)
            objects.addNode(node)
            print(objects.toString())
        }
        .padding()
        .foregroundColor(.white)
        .background(.red)
    }
}

The console unexpectedly shows me this:


31042C43-103A-48F3-B926-47A5B2CB9610:(50.0, 200.0)
1
6B95D75C-8849-4840-AB0F-56BEED00D73F:(200.0, 100.0) 
1
Nodes: (0)

Now, pressing the "Add" button, the array count increases starting from 1.

If I change the declaration of the variable from

@StateObject var objects = Objects()

to

@State var objects = Objects()

the initialization goes as expected:

6A2A5EFF-1A7F-4995-B2D6-DC22537FB4D1:(50.0, 200.0)
1
A118917D-6635-4E4E-AED0-B865AF2DA81C:(200.0, 100.0)
2
Nodes: (2)
6A2A5EFF-1A7F-4995-B2D6-DC22537FB4D1:(50.0, 200.0)
A118917D-6635-4E4E-AED0-B865AF2DA81C:(200.0, 100.0)

and the array count starts from 3 when I press the “Add” button.

Unfortunately, I need to share the object across views and the @StateObject is necessary. Can you suggest a solution and explain to me why this appens?

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
Antonio
  • 105
  • 8
  • Does this answer your question https://stackoverflow.com/a/62636048/12299030? – Asperi Jun 29 '22 at 18:38
  • 1
    Trying adding the init in objects instead of the contentView – Luis Ramirez Jun 30 '22 at 02:24
  • @Asperi Browsing the thread you suggested, I understood that a StateObject actual initialization is performed after the execution of the view init() routine, if I understood correctly. This would explain why I end up with an initialized but empty object whatever I tried to do in init(). – Antonio Jun 30 '22 at 09:48

2 Answers2

1

As referenced in comment here is a fixed variant (tested with Xcode 13.4 / iOS 15.5)

struct ContentView: View {
    @StateObject var objects: Objects

    init() {
        let objects = Objects()   // << created
        var node = Node()
        node.x = 50
        node.y = 200
        print(node.toString())
        objects.addNode(node)
        node = Node()
        node.x = 200
        node.y = 100
        print(node.toString())
        objects.addNode(node)
        objects.show.toggle()
        print(objects.toString())

        // Note: don't believe Apple's documentation about this initializer
        // and those on SO who shout about it ;-)
        _objects = StateObject(wrappedValue: objects)    // << here !!
    }
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • All that needs to go inside the @autoclosure wrappedValue: { here } see https://developer.apple.com/documentation/swiftui/stateobject/init(wrappedvalue:) – malhal Jun 29 '22 at 23:58
0

Objects aren't necessary for sharing data. When passing into a child View declare a let for read access or @Binding for write access. body is called if the param is different from last time the View as init.

Try an @State struct, use a mutating func to modify it like an object.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • You’re actually right. I need an object because I’ve to be sure that a unique copy of the item is shared among many views, but I didn’t mention this in my question. My fault. – Antonio Jun 30 '22 at 14:08