Untraceable SIGABRT: precondition failure: attribute failed to set an initial value: <num>

Edit: Small changes to posted code to allow easier testing in tmp project. Create a new Single View App that using SwiftUI. C&P View A into ContentView.swift, and View B into a new files called "Dots"


I have also found that the issue does not occur when passing 0 to Dots, and have added that to the picker to aid testing.


I have spent countless hours doing my best to solve this. Read through too many other posts about this issue being caused by a lot of different things, hacked and slashed code and simple print statement/lldb to try track it down and I'm getting now - I'm pretty frantic right now.


This is the same core problem as a previous post

View works fine until used in NavigationView/Link


Whilst the workaround stopped the crashes, it was definitely a workaround and not fixing the source. As a consequence, a different "bug" appeared why a custom view partially ignored safe boundaries*. Rather than trying to find a hacky workaround for the workaround, it would be far better to try solve the core issue, but am deeply struggling.


As explained in the previous post, View A and B work fine on their own, but when navigation from A to B, that when it fails. Stepping through the code didn't yield much but it be a case of not knowing what to look for.


* this view consisted of x views that behaved like horizontal cards, swiping left/right to jump between them. This was achieved by having a HStack(spacing: 0) with a frame x times the width of the screen and a custom Gesture. If you envisage the views as an array, index 0 would be shown by default, and one can swipe amongst them. The first 2 items would correctly respect the inherited height, but 3 onwards would go behind the navigation space and status bar.


Note I've attempted to clear the code of some unnecessary content so as to avoid wasting your time.


View A (Note I've attempted to clear the code of some unnecessary content so as to avoid wasting your time.)

import SwiftUI
  
let lowerBound = 0
let upperBound = 101

struct CapsuleButton: ViewModifier {
    func body(content: Content) -> some View {
        content
            .frame(width: 250, height: 100)
            .background(Capsule().fill(Color.blue))
            .foregroundColor(.black)
            .font(.system(size: 28, weight: .light))
            .padding()
    }
}

struct ContentView: View {
    @State private var pickerIndex: Int = UserDefaults.standard.integer(forKey: "highestDotCovered") - lowerBound
    @State private var nav: Int? = 0
      
    var body: some View {
        VStack(spacing: 30) {
            HStack {
                Picker(selection: $pickerIndex, label: Text("")) {
                    ForEach(lowerBound..<upperBound) { Text(String($0)) }
                }
            }
            Spacer()
            NavigationLink(destination: Dots(lowerBound + pickerIndex), tag: 1, selection: $nav) { EmptyView() }
  
            Text("Continue")
                .modifier(CapsuleButton()) // custom look
                .onTapGesture {
                    // Update storage
                    let currentHighest = UserDefaults.standard.integer(forKey: "highestDotCovered")
                    let highest = max(lowerBound + self.pickerIndex, currentHighest)
                    UserDefaults.standard.set(highest, forKey: "highestDotCovered")
                    // Go to destination
                    self.nav = 1
                }
        }
        .navigationBarTitle("Quantity Selection")
    }
}
  
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            ContentView()
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}


View B

import SwiftUI
  
private let diam: CGFloat = 60 // ~1/3", 1pt = 1/163"
private let maxPhoneDots = 12
  
struct Dots: View {
    @State private var dotAlert = false
      
    let quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)
    @State var offsets: [CGSize] = []
      
    init(_ quantity: Int) {
        let model = UIDevice().model
        self.quantity = model == "iPad" ? quantity : min(quantity, maxPhoneDots)
        print(self.quantity)
    }
  
    var body: some View {
//        NavigationView { // need to wrap geom reader in nav view to avoid crash
//            VStack {
                GeometryReader { geom in
                    ForEach(self.genOffsets(maxWidth: geom.size.width, maxHeight: geom.size.height), id: \.self) {offset in
                        self.dot
                            .offset(offset)
                    }
                    .background(Color.clear
                        .contentShape(Rectangle())) // workaround for allowing gestures on clear background
                }
                .border(Color.black)
                  
                //            Spacer()
//                Text("Dots: \(self.quantity)")
//            }
//            .navigationBarTitle("")
//            .navigationBarHidden(true)
//        }.navigationViewStyle(StackNavigationViewStyle())
    }
}
  
extension Dots {
    func genOffsets(maxWidth: CGFloat, maxHeight: CGFloat) -> [CGSize] {
        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 offsets: [CGSize] = []
  
        let availableWidth = max(maxWidth - (CGFloat(cols) * diam), 0)
        let colSeparation: CGFloat = availableWidth / CGFloat((cols+1))
        let availableHeight = max(maxHeight - (CGFloat(rows) * diam), 0)
        let rowSeparation: CGFloat = availableHeight / CGFloat((rows+1))
          
        let xJitter = colSeparation * 0.49
        let yJitter = rowSeparation * 0.49
  
        // 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
  
            // Calculate dot's offset
            //      Initial padding from TL corner
            var x = colSeparation
            var y = rowSeparation
            //      Separate dot into standard grid slot
            x += CGFloat(c)*(diam+colSeparation)
            y += CGFloat(r)*(diam+rowSeparation)
            //      Apply jitter
            if xJitter != 0 { x += CGFloat.random(in: -xJitter ..< xJitter) }
            if yJitter != 0 { y += CGFloat.random(in: -yJitter ..< yJitter) }
  
            offsets.append(CGSize(width: x, height: y))
        }
          
        return offsets
    }
}
  
extension CGSize: Hashable {
    public func hash(into hasher: inout Hasher) {
      hasher.combine(width)
      hasher.combine(height)
    }
}

struct Dots_Previews: PreviewProvider {
    static var previews: some View {
        Dots(1)
    }
}

Accepted Reply

Thanks for showing an easily testable code.

With your updated code, I have tried a few things and I guess some SwiftUI structs (in this case ForEach) does not like changing its structure based on some unspecified states.


You may have tried already, but using an explicitly declared `@State` seems to solve the issue.


For your ViewB:

import SwiftUI

private let diam: CGFloat = 60 // ~1/3", 1pt = 1/163"
private let maxPhoneDots = 12

struct Dots: View {
    let quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)

    @State var positions: [DotPosition] = [] //<-
    
    init(_ quantity: Int) {
        let model = UIDevice().model
        self.quantity = model == "iPad" ? quantity : min(quantity, maxPhoneDots)
        print(self.quantity)
        self._positions = State(initialValue: 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))
            }
            .background(Color.clear
                .contentShape(Rectangle())) // workaround for allowing gestures on clear background
        }
        .border(Color.black)
    }
}

struct DotPosition: Hashable {
    var r: Int
    var c: Int
    var jitterScaleX: CGFloat
    var jitterScaleY: CGFloat
}

extension Dots {
    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
    }
}
extension DotPosition {
    func offset(maxWidth: CGFloat, maxHeight: CGFloat) -> CGSize {
        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
        }
        
        let availableWidth = max(maxWidth - (CGFloat(cols) * diam), 0)^
        let colSeparation: CGFloat = (availableWidth / CGFloat((cols+1)))^
        let availableHeight = max(maxHeight - (CGFloat(rows) * diam), 0)^
        let rowSeparation: CGFloat = (availableHeight / CGFloat((rows+1)))^
        
        let xJitter = colSeparation * 0.49
        let yJitter = rowSeparation * 0.49
        
        // Calculate dot's offset
        //      Initial padding from TL corner
        var x = colSeparation
        var y = rowSeparation
        //      Separate dot into standard grid slot
        x += CGFloat(c)*(diam+colSeparation)
        y += CGFloat(r)*(diam+rowSeparation)
        //      Apply jitter
        if xJitter != 0 { x += jitterScaleX * xJitter }
        if yJitter != 0 { y += jitterScaleY * yJitter }
        
        return CGSize(width: x, height: y)
    }
}

Replies

but when navigation from A to B, that when it fails.

Please detail:

- what do you get ?

- what did you expect ?


That would help to be able to run the project.

=> Could you post the rest of code, notably ContentView file ?

Run Debug Preview on View A. When clicking "Continue", it should show View B. Instead, the terminal shows precondition failure: attribute failed to set an initial value: 578 (the number varies). Then in the editor, it auto navigates to the boiler plate AppDelegate to show the error:

class AppDelegate: UIResponder, UIApplicationDelegate { // <- Thread 1: signal SIGABRT


For this purpose View A can be ContentView, so I'll quickly edit the post to allow you to run in a tmp xCode project.

I tested.


No crash, but nothing occurs when I tap continue.


But if I replace

emptyView()

by

Dots(lowerBound + pickerIndex)


Then I see dots, with apparently the correct number of dots.


Something is not working in this code.

Do you run in Preview or on device simulator or on device ?

But, if I change the body of ContentView with


    var body: some View {
        NavigationView { // need to wrap geom reader in nav view to avoid crash
        VStack(spacing: 30) {

Then I get the crash.


So, what is the exact code to test ?

With the code as posted, testing in Preview. Live preview, continue goes to a blank screen, in Debug preview, the described error occurs.

I just ran a simulator and I get the behaviour of Continue now behaving as you described as well.

What do you expect Continue to do ?

It activates the associated NavigationLink.


It's like having

NavigationLink(destination: Dots(lowerBound + pickerIndex)) { 
     Text("Continue").modifier(CapsuleButton())
}


but allows some extra code to be executed in between the user tapping the link, and the actually navigating to the destination i.e. Dots(num). In my case, it updates the stored UserDefaults. I believe the inspiration came from the solution for https://stackoverflow.com/questions/57130866/how-to-show-navigationlink-as-a-button-in-swiftui


The issue still occurs if you have the above instead of


NavigationLink(destination: Dots(lowerBound + pickerIndex), tag: 1, selection: $nav) { EmptyView() 
    
Text("Continue")
     .modifier(CapsuleButton()) // custom look
     .onTapGesture {
          // Update storage
          let currentHighest = UserDefaults.standard.integer(forKey: "highestDotCovered")
          let highest = max(lowerBound + self.pickerIndex, currentHighest)
          UserDefaults.standard.set(highest, forKey: "highestDotCovered")
          // Go to destination
          self.nav = 1
     }

Well, so it seems to me that it is doing what expected (saves to user defaults).


What is not clear is what is expected from line 11

self.nav = 1

Indeed, user defaults was never the problem?


That line triggers the associated NavigationLink, ie telling the NavigationLink instance that has the "nav" variable as the value for its "selection" property/parameter. You don't need to fixate on it, as I showed in the last post, the crash is unaffected if you remove it and use a basic NavigationLink that have something instead of the EmptyView().


When you said earlier that replacing EmptyView() with Dots() works - this is not really related. What is important is that Dots() is the value of the NavigationLink's "destination", and when trying to navigate to that destination, with a dot quantity value other than 0, it crashes. Furthermore, simply including the same Dots() view on the page (like when you replaced EmptyView() with it) does not trigger the crash. Thus it is strictly related to navigating to the Dots view.


Does that help clarify the issue at all?

Thanks for showing an easily testable code.

With your updated code, I have tried a few things and I guess some SwiftUI structs (in this case ForEach) does not like changing its structure based on some unspecified states.


You may have tried already, but using an explicitly declared `@State` seems to solve the issue.


For your ViewB:

import SwiftUI

private let diam: CGFloat = 60 // ~1/3", 1pt = 1/163"
private let maxPhoneDots = 12

struct Dots: View {
    let quantity: Int
    let dot: some View = Circle()
        .size(width: diam, height: diam)
        .fill(Color.red)

    @State var positions: [DotPosition] = [] //<-
    
    init(_ quantity: Int) {
        let model = UIDevice().model
        self.quantity = model == "iPad" ? quantity : min(quantity, maxPhoneDots)
        print(self.quantity)
        self._positions = State(initialValue: 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))
            }
            .background(Color.clear
                .contentShape(Rectangle())) // workaround for allowing gestures on clear background
        }
        .border(Color.black)
    }
}

struct DotPosition: Hashable {
    var r: Int
    var c: Int
    var jitterScaleX: CGFloat
    var jitterScaleY: CGFloat
}

extension Dots {
    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
    }
}
extension DotPosition {
    func offset(maxWidth: CGFloat, maxHeight: CGFloat) -> CGSize {
        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
        }
        
        let availableWidth = max(maxWidth - (CGFloat(cols) * diam), 0)^
        let colSeparation: CGFloat = (availableWidth / CGFloat((cols+1)))^
        let availableHeight = max(maxHeight - (CGFloat(rows) * diam), 0)^
        let rowSeparation: CGFloat = (availableHeight / CGFloat((rows+1)))^
        
        let xJitter = colSeparation * 0.49
        let yJitter = rowSeparation * 0.49
        
        // Calculate dot's offset
        //      Initial padding from TL corner
        var x = colSeparation
        var y = rowSeparation
        //      Separate dot into standard grid slot
        x += CGFloat(c)*(diam+colSeparation)
        y += CGFloat(r)*(diam+rowSeparation)
        //      Apply jitter
        if xJitter != 0 { x += jitterScaleX * xJitter }
        if yJitter != 0 { y += jitterScaleY * yJitter }
        
        return CGSize(width: x, height: y)
    }
}

Amazing, thank you!


Some great ideas in code reorganisation here as well, namely separating the random aspects of a dot's position from the bit requiring the GeomertryReader values, which neatly eliminates the forced conformance of CGSize to Hashable. My initial approach had me thinking quite linearly.


May I ask what difference between

self._positions = State(initialValue: createPositions())

and just

self.positions = createPositions()

is?


Thanks again.

Unfortunately, @State variables are not ready while in init(_:).

SwiftUI allocates actual storage for @State variables just before `body` is evaluated,

until then, any assignments are simply ignored.


You can find that no Dots are drawn if you use `self.positions = createPositions()`.


Assigning `State` to the underlying wrapper property (`_positions`) is a technique to initialize an @State variable with some calculation result.