New View Instance Uses Old Values

(This post follows on from the solution to Untraceable SIGABRT: precondition failure: attribute failed to set an initial value: <num>)


Hopefully one of the last posts I make as I sprint toward this project's end. The issue involves a lot of code that wouldn't be practical to paste here, so I'll do my best to simplify some and leave other bits out completely. If you need the full thing I could maybe email the relevant files but for now hopefully this will do.


I have a Dots(_:) view which takes a single parameter, quantity. There is then a chunk of complicated looking code that ultimately randomly places them on screen - this bit has been left out of the post.


struct Dots: View {
    @State private var positions: [DotPosition] = []
    
    let quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)

    init(_ quantity: Int) {
        self.quantity = quantity

        self._positions = State(initialValue: createPositions())
        for pos in positions { print(pos)} // <- this is showing that new values ARE being made
    }

    var body: some View {
        GeometryReader { geom in
            ForEach(self.positions, id: \.self) {position in
                self.dot
                    .offset(position.offset(maxWidth: geom.size.width, maxHeight: geom.size.height)) 
                    // ^ a print statement in here shows iteration over the new values, and then the very first values
            }
        }
    }
}


The file that could effectively be used as ContentView/root for this problem, again simplified, is

struct SwipableEquation: View {
    let num: Int

    var body: some View {
        VStack {
            GeometryReader { geo in
                HStack(spacing: 0) {
                    Group {
                        Dots(self.num) // <- prints show a new instance gets created when exepcted, but the view doesn't change
                    }
                }
            }
            Text(String(num)) // <- this value actively changes as and when expected
        }
    }
}

struct ContentView: View {

    var nums: [Int] = [2,3,4]
    @State private var currentEqn = 0
    
    
    var body: some View {
        VStack {
            SwipableEquation(num: nums[currentEqn])
            
            HStack {
                // Previous Eqn
                Button(action: {
                    self.currentEqn -= 1
                    print("\n######### BACK #########")
                }) {
                    Image(systemName: "chevron.left")
                    VStack {
                        Text("Previous Equation")
                        if currentEqn > 0 {
                            Text(String(nums[currentEqn-1]))
                        }
                    }
                }.disabled(self.currentEqn == 0)

                Spacer()
                
                // Next eqn
                Button(action: {
                    self.currentEqn += 1
                    print("\n######### NEXT #########")
                }) {
                    VStack {
                        Text("Next Equation")
                        if currentEqn < nums.count-1 {
                            Text(String(nums[currentEqn+1]))
                        }
                    }
                    Image(systemName: "chevron.right")
                }.disabled(self.currentEqn == nums.count-1)
            }.padding()
        }
    }
}

Accepted Reply

You should better include the link to your old thread.


Very interesting behavior.

As far as I tested, it is a problem of `@State` variables.

Please try this:

struct Dots: View {
    private var positions: [DotPosition] = [] //<- Remove `@State`
      
    var quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)
  
    init(_ quantity: Int) {
        self.quantity = quantity
  
        self.positions = createPositions()
    }
  
    var body: some View {
        GeometryReader { geom in
            ForEach(self.positions, id: \.self) {position in
                self.dot
                    .offset(position.offset(maxWidth: geom.size.width, maxHeight: geom.size.height))
            }
        }
    }
}


Many parts of SwiftUI are hidden and we need to guess what's going bihind, but I guess it's related to the strategy of SwiftUI to allocate actual storage of `@State` variables. When there's an `@State` variable with its storage already allocated, SwiftUI continues to use the allocated storage and ignores `initialValue` of `State`.

Replies

An example of prints made during quick run in Xcode preview. As you can see, the underlined components are repeated throughout and are the values that actually get used in the view being shown. None of the other positions ever appear.


To be clear, after Next has been tapped, the new Dots(_:) instance should only be using the position values seen under "new positions" before the empty newline.


// Hit Debug preview

new positions

DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)



DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)

DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)


new positions

DotPosition(r: 0, c: 2, jitterScaleX: -0.26761998822086364, jitterScaleY: -0.21409538770110936)

DotPosition(r: 3, c: 2, jitterScaleX: 0.2007303590288385, jitterScaleY: -0.48008864864731593)



DotPosition(r: 0, c: 2, jitterScaleX: -0.26761998822086364, jitterScaleY: -0.21409538770110936)

DotPosition(r: 3, c: 2, jitterScaleX: 0.2007303590288385, jitterScaleY: -0.48008864864731593)

DotPosition(r: 3, c: 2, jitterScaleX: 0.2007303590288385, jitterScaleY: -0.48008864864731593)

DotPosition(r: 0, c: 2, jitterScaleX: -0.26761998822086364, jitterScaleY: -0.21409538770110936)


// Finish initial load, now interacting...

######### NEXT #########


new positions

DotPosition(r: 2, c: 2, jitterScaleX: 0.8712347388399257, jitterScaleY: 0.9516460599809775)

DotPosition(r: 2, c: 3, jitterScaleX: 0.2477516584036734, jitterScaleY: 0.45822677865038264)

DotPosition(r: 1, c: 3, jitterScaleX: 0.30958523909508173, jitterScaleY: -0.7122354433992328)



DotPosition(r: 2, c: 2, jitterScaleX: 0.8712347388399257, jitterScaleY: 0.9516460599809775)

DotPosition(r: 2, c: 3, jitterScaleX: 0.2477516584036734, jitterScaleY: 0.45822677865038264)

DotPosition(r: 1, c: 3, jitterScaleX: 0.30958523909508173, jitterScaleY: -0.7122354433992328)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)

DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)


######### NEXT #########


new positions

DotPosition(r: 4, c: 2, jitterScaleX: -0.29471233845995637, jitterScaleY: 0.9377753107486326)

DotPosition(r: 1, c: 2, jitterScaleX: 0.14988943796420284, jitterScaleY: -0.8542303116630483)

DotPosition(r: 2, c: 1, jitterScaleX: -0.07679392843163457, jitterScaleY: 0.4895482036137371)

DotPosition(r: 3, c: 1, jitterScaleX: -0.7264365110057209, jitterScaleY: -0.7451452602829394)



DotPosition(r: 4, c: 2, jitterScaleX: -0.29471233845995637, jitterScaleY: 0.9377753107486326)

DotPosition(r: 1, c: 2, jitterScaleX: 0.14988943796420284, jitterScaleY: -0.8542303116630483)

DotPosition(r: 2, c: 1, jitterScaleX: -0.07679392843163457, jitterScaleY: 0.4895482036137371)

DotPosition(r: 3, c: 1, jitterScaleX: -0.7264365110057209, jitterScaleY: -0.7451452602829394)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)

DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)


######### BACK #########


new positions

DotPosition(r: 2, c: 3, jitterScaleX: 0.8320728275072029, jitterScaleY: -0.8453647178718258)

DotPosition(r: 1, c: 3, jitterScaleX: 0.5753115798672581, jitterScaleY: -0.8042377280170807)

DotPosition(r: 1, c: 2, jitterScaleX: 0.28010870718446235, jitterScaleY: 0.11277668898845605)



DotPosition(r: 2, c: 3, jitterScaleX: 0.8320728275072029, jitterScaleY: -0.8453647178718258)

DotPosition(r: 1, c: 3, jitterScaleX: 0.5753115798672581, jitterScaleY: -0.8042377280170807)

DotPosition(r: 1, c: 2, jitterScaleX: 0.28010870718446235, jitterScaleY: 0.11277668898845605)

DotPosition(r: 3, c: 1, jitterScaleX: -0.405571513766956, jitterScaleY: -0.5655123592406626)

DotPosition(r: 1, c: 0, jitterScaleX: -0.9684827153965649, jitterScaleY: 0.33412677927887224)

You should better include the link to your old thread.


Very interesting behavior.

As far as I tested, it is a problem of `@State` variables.

Please try this:

struct Dots: View {
    private var positions: [DotPosition] = [] //<- Remove `@State`
      
    var quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)
  
    init(_ quantity: Int) {
        self.quantity = quantity
  
        self.positions = createPositions()
    }
  
    var body: some View {
        GeometryReader { geom in
            ForEach(self.positions, id: \.self) {position in
                self.dot
                    .offset(position.offset(maxWidth: geom.size.width, maxHeight: geom.size.height))
            }
        }
    }
}


Many parts of SwiftUI are hidden and we need to guess what's going bihind, but I guess it's related to the strategy of SwiftUI to allocate actual storage of `@State` variables. When there's an `@State` variable with its storage already allocated, SwiftUI continues to use the allocated storage and ignores `initialValue` of `State`.

wow, I spent hours testing things but aparently removing the state wrapper for positions wasn't one of them... ooft.


I'm just baffled that a completely separate instance was able to (and more importantly only) access the other's values. The blindness and fairly empty documentation of SwiftUI have been a recurring theme for struggles in this project. Depsite the many great concepts and features it has, hindsight has definitely shown I should have chosen a more mature approach/framework for my first foray into apps. Thanks for all your help OOPer.


Unfortunately, this fix undoes the fix from the earlier post, and thus in the larger scheme of the app, becomes unusable as navigating to a Dots(_:) instance causes the SIGABRT crash error.


I'll check this post in the morning but I'm thinking I'll just have to change the approach and rather than having a button changing an index (which signifies what the current item is), it might have to be a NavigationLink or something.

I'm just baffled that a completely separate instance was able to (and more importantly only) access the other's values

You know SwiftUI evaluates `body` at anytime it thinks is needed, which means all the sub-Views generates a new instance at each time `body` is re-evaluated, but @State variables inside such sub-Views need to keep their state between the re-evaluations.


Unfortunately, this keeping-states functionality is not well-documented and in this case, `positions` is kept regardless of the changes of `quantity`.

As you said, the documentation is almost empty and we need to study by ourselves.


As far as I tried, the following `Dots` works as expected in more cases.

struct Dots: View {
    @ObservedObject var positionGenerator: PositionGenerator
      
    var quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)
  
    init(_ quantity: Int) {
        self.quantity = quantity
  
        self.positionGenerator = PositionGenerator(quantity)
    }
  
    var body: some View {
        GeometryReader { geom in
            ForEach(self.positionGenerator.positions, id: \.self) {position in
                self.dot
                    .offset(position.offset(maxWidth: geom.size.width, maxHeight: geom.size.height))
            }
        }
    }
}

class PositionGenerator: ObservableObject {
    @Published var positions: [DotPosition] = []
    
    private var quantity: Int
    
    init(_ quantity: Int) {
        self.quantity = quantity
        self.positions = createPositions()
    }
    
    func createPositions() -> [DotPosition] {
        var cols = UIDevice().model == "iPad" ? 8 : 4
        var rows = UIDevice().model == "iPad" ? 13 : 5
        if UIDevice().orientation.isLandscape {
            (cols, rows) = (rows, cols) //swap cols and rows
        }
        
        var positions: [DotPosition] = []
        
        // grid[row][col]
        var grid = Array(repeating: Array(repeating: false, count: cols), count: rows)
        
        // Generate dot offsets
        for _ in 0..<quantity {
            var r, c: Int
            // Find a random empty cell
            repeat {
                r = Int.random(in: 0..<rows)
                c = Int.random(in: 0..<cols)
            } while grid[r][c]
            grid[r][c] = true
            
            //      Apply jitter
            let jitterScaleX = CGFloat.random(in: -1 ..< 1)
            let jitterScaleY = CGFloat.random(in: -1 ..< 1)
            
            positions.append(DotPosition(r: r, c: c, jitterScaleX: jitterScaleX, jitterScaleY: jitterScaleY))
        }
        
        return positions
    }
}

Found a simpler implementation.

struct Dots: View, DynamicViewContent {
    var positions: [DotPosition] = []
    
    var data: [DotPosition] {positions}
      
    var quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)
  
    init(_ quantity: Int) {
        self.quantity = quantity
  
        self.positions = createPositions()
    }
  
    var body: some View {
        GeometryReader { geom in
            ForEach(self.positions, id: \.self) {position in
                self.dot
                    .offset(position.offset(maxWidth: geom.size.width, maxHeight: geom.size.height))
            }
        }
    }
}

Seems SwiftUI, nearly one year since first introduced, has many rooms to explore.

Nice find, though I'm not entirely sure what effect it has, especially as the new 'data' property isn't explicitly called. How did you find it? But like you say, with the lack of documentation, one just has to explore and experiment. From my brief look at functions associated with DynamicViewContent, it may have been a cleaner way to implement my horizontal swipable view, but I digress.


Unfortunately, when two other components are reintroduced (only when they are both there, not either or), SIGABRT's still occur. However, unless we find a way to ensure a 'state' var updates, this is getting further and further away from the original thread topic (which you explained was simply due to SwiftUI's states strategy) so I should probably leave it here and pursue other options.

How did you find it?

Finding the protocol DynamicViewContent, explored how it would work, as always in SwiftUI.


Anyway, thanks for showing a good example to explore. Expecting we can find something better next time.